[펌] 알파블랜드

게임 라이브러리에는 그런 효과를 많이 요하니까 소스가 많거든요
제가 알고 있는 cdx( directx api를 클래스화 한라이브러리)에 있는 
alphablend blit에 보면 16비트 비트맵을 서로 중간색내는것 소스도 있겠고요
(아참 비트맵을 DIB비트맵으로 만드시고 DIB비트맵에서 해당 픽셀 데이타로
직접 접근 하시는건 아시죠? 그상태에서 말하는겁니다..)
아래는 예전에 하이텔에서 갈무리한 내용이고요..
그리고 더 밑에다가는 또 역시 좋은 강좌 ( 여러 효과)도 같이 올려보도록하져..
강좌를 쓰신 분들께 항상감사한 마음으루~~ (근데 이게 언제쩍이냥 --;)


..more


칼루
나만의 강의 2004. 7. 25. 22:54
,

[펌][ 오카모토 요시키 ]의 기획 강의

계속 업데이트 된다고 하여, 그냥 주소를 띄어 왔음^^;

 

http://blog.naver.com/fallskya/100004377064

칼루
나만의 강의 2004. 7. 25. 18:31
,

[펌] 자동으로 그림자를 만들어 보자

자동으로 그림자를 만들어 보자

오늘은 2D게임에서 캐릭터의 그림자를 자동으로 생성하는 기법을 이야기 해 보려고 한다.

이 기법은 1996년 12월에 한겨레정보통신(현 DDS)에서 처음으로 4명이 팀을 이루어 에스퍼라는
RPG를 제작하게 되면서 개발하게 된 것이다. 많지 않은 인원이 3개월만에 RPG를 만들어야
했었던 지라 모두가 고분분투하고 있었고 캐릭터작업은 한명이 담당하여 작업하고 있었다.
그러나 캐릭터의 양이 많아서 그림자에 대한 생각은 엄두도 내지 못하고 있었다.

여기서 잠깐 캐릭터의 그림자방식에 대해 이야기해 보죠. 일반적으로 캐릭터의 그림자방식에는
단순식과 실사식으로 나누어 이야기할수 있다. 단순식은 그저 캐릭터 밑에 검은 타원이 그림자처럼
존재하는 방식이고 실사식은 캐릭터의 모습을 비슷하게 닮은 검은 물체를 그림자로 사용하는 방식이죠.  

그래서 의논을 해 보았으나 그림자를 단순식으로 만들 수 밖에 없는 상황이었다. 또한 단순식도
그리 만만치 않아서 캐릭터디자이너가 해야 할 단순 작업이 너무 많았다. 캐릭터밑에 일일이
검은 타원을 붙여야 하기 때문이었죠.

그러나 이런 일들은 프로그래머가 조금만 시간을 내 캐릭터밑에 검은 타원을 만들어 주는 프로그램을
제작해주면 간단하게 해결되는 일이었다. 물론 내가 왜 캐릭터 그림자까지 신경써야 하지? 라는 생각을
가지는 프로그래머도 있을 수 있겠다. 만약 그러한 게임 프로그래머가 있다면 절대 게임을 제작하지
말고 다른 업종을 생각해 보기 바란다. 게임은 너가 할 일 따지고 내가 할 일 따지면 절대 좋은 작품이
나올 수 없기 때문이다.

필자는 여기서 좀더 진지하게 고민해 보았다. 물론 단순식으로 제작하는 것은 최후의 일이고
더 좋은 방법은 없을까?하고 생각한 것이다. 프로그램적으로 실사처럼 그림자를 만들어 준다면
이보다 좋은 방법은 없을 것이라 생각했다. 며칠 고민하니 좋은 생각이 떠올랐고 그 아이디어를
밑바탕으로 그림자 생성 프로그램을 만들어 봤더니 아주 괜찮았다. 그 기법을 이제 공개하겠다.
(그전부터 공개하려고 하였으나 계속 바뻐 글을 쓸 시간이 없었다. 핑계없는 무덤이 어디 없는가?)

서두에 말이 많았는데, 이 기법의 주된 바탕은 두 직선의 방정식을 토대로 하고 있다.

그림1

위의 그림처럼 캐릭터가 서있는 바닥이 있고 빛의 방향이 있다면 캐릭터의 한점 한점은 빛을 받아
바닥에 그려지게 된다. 이게 바로 그림자가 되는 것이다.

그림을 차근 차근 그려보면서 알아보자. 아래와 같은 그림에서 물건의 그림자를 그려보자.

 

물건의 상위의 첫 점인 빨간색은 빛의 방향에 따라 내려가다가 바닥의 선과 만나는 곳에 찍으면 된다.
그러면 다음과 같이 된다.

그 다음 점인 파란색도 그런식으로 하면 다음과 같이 되고

마찬가지로 이런 식으로 물건을 전부 그려 나가다 보면 다음과 같이 된다.

여기서 바닥과 만날 때 정해진 그림자 색깔로 그리면 바로 그림자가 되는 것이다.

 자. 어떠한가? 이론은 정말 간단하지 않은가? 

아래의 그림처럼 원래의 캐릭터그림을 회전시켜 만든 그림자하고는 차원이 다르다.

 

이 이론을 한번 공식으로 정립해 보자. 빛의 방향을 하나의 직선으로 생각하고 바닥또한
하나의 직선으로 생각해 보자. 원래의 점은 빛의 직선에 있고 그림자는 바닥의 직선에
생기므로 두 직선의 교차점을 찾으면 그것이 바로 그림자가 되겠다.

빛의 직선은  y1 = a * x1 + b 이라 하고

바닥의 직선을  y2 = c * x2 + d 라고 하자.

그러면 여기서 a는 빛의 각도를 알면 구할수 있게 된다. 빛의 각도를 LightAngle이라고 하면,
a는 직선의 기울기이므로  
a = tan(3.141592 * LightAngle / 180) 이 된다.

여기서 주의할 것은 PC스크린 상의 좌표는 Y가 밑으로 갈수로 +가 된다는 사항이다. 원래의
좌표계를 뒤집어 생각해야 한다.

그리고 c = -tan(3.141592 * ShadowAngle / 180) 이 된다. ShadowAngle은 바닥의 각도이고,

tan(360 - 각도) = -tan(각도) 이므로 - 값이 붙는다.

 

그럼 두 직선의 교점을 알아보자.

-)    y = c * x + d

- )   y = a * x + b

       x = (d - b ) / (a - c) 가 된다.   여기서 x 값을 구해 빛의 직선에 대입해 보면 y 값도 알게 되서

교점을 알수 있게 된다. 이 교점이 바로 한점의 그림자가 된다.

그럼 이제 실질적인 소스를 보면서 진행해 보자. 그러기 위해서 먼저 몇가지만 정의하고 가자.

  1. 데이타는 256 칼라이다. 
  2. 투과색 : TransColor, 그림자색은 : ShadowColor
  3. 3.141592 = PAIVALUE

BOOL BufMakeShadow(unsigned char*  buf, int width, int height) {
     int x, y;
     int address;
     int bottom;

     address = 0;
     bottom = 0; 
// 이 부분은 buf내에서 물체의 제일 밑부분을 알아내는 부분
     for(y = 0; y < height; y++) {
         for(x = 0; x < width; x++) {
             if(buf[address++] != TransColor) {
                 bottom = y;
             }
         }
     } 

     int data;
     double dA, dB, dC, dD, dX, dY;
     int mx, my;

// 빛의 기울기와 바닥의 기울기 계산
     dA = tan(PAIVALUE * LightAngle / 180);
     dC = -tan(PAIVALUE * ShadowAngle / 180);

     for(y = 0; y < bottom; y++) {
         for(x = 0; x < width; x++) {
             address = y * width + x;
             data = buf[address];        

             if(data != TransColor && data != ShadowColor) {
                 dB = y - dA * x;
                 dD = bottom - dC * j;

                 dX = (dD - dB) / (dA - dC);
                 dY = dA * dX + dB;

                           // 교점 계산
                 mx = (int)dX;     
                 my = (int)dY;         

                 if(mx < 0 || my < 0 || mx >= width || my >= bottom) {
                                 // 그림자가 버퍼내에 생성이 안된다면 빛의 각도나 바닥의 각도가 너무 크거나 작던지
                                 // 버퍼가 너무 작던지 하는 문제이다. 될수록 그림내에 캐릭터는 중앙에 위치하고 그림이
                                 // 캐릭터보다 2-3배정도는 커야 그림자를 그리기 수월하다
                     return FALSE;
                 }          

                 address = my * width + mx;
                 data = buf[address];
                 if(data == TransColor) {
                         /* 
                                           그림자를 격자로 만들려면...
                          if(y & 1) {
                         if(x & 1) buf[address] = ShadowColor;                  
                     } else {
                         if(!(x & 1)) buf[address] = ShadowColor;
                     }

                                  */
                     buf[address] = ShadowColor; 
                 }

             }
         }
     }
     return TRUE;
}

그리고 그림자를 생성할 때 다음과 같이 그림자를 격자형태로 만들 수도 있겠다.

이렇게 하면 그림자를 그릴 때 바닥이 보이는 면이 있어서 투과되어 보이는 것처럼
느낄수 있게된다. 물론 프로그램에서 그림자만 따로 저장하여 그림자를 그릴 때 바닥과
오버레이를 줄수도 있지만, 이렇게 하면 CPU가 바쁘게 되므로 잘 생각해서 선택하는 것이
중요하겠다.

마지막으로 해보면 알겠지만, 이 기법은 만능이 아님을 미리 밝힌다. 사람이 아닌 것(두발이 아닌
네발짐승이나 몬스터)이라든지, 특이한 동작을 하는 캐릭터는 그림자가 이상하게 생기며 샘플
프로그램에 사용된 소스에는 하나의 직선을 더 사용해서 보정의 기능이 들어가 있음을 밝힌다.

다만, 소스를 공개하는 만큼 사용자가 필요한 부분에서는 조금씩 변경하여 사용하면 될 것으로
사료된다.

샘플프로그램 다운로드

칼루
나만의 강의 2004. 7. 18. 15:42
,

이력서 - 자기소개서

꽤 전에 "게임잡"에 써 올렸던 이력서 내용중 "자기소개서" 부분의 내용이다.

이걸로 교찬이형이랑 인연이 닿아, PMG에 다니고 있다. 스터디그룹이지만 실제로 이 이력서를 써 올리면서도 내가 바라는 (아르바이트 자리 정도)건 되지 않을꺼라 생각되었다.

 

이건 현실적으로 생각해보면 답은 빠르게 나온다. 과연 게임프로그램 부분에서 알바할 만한 일거리가 있느냐?? 이 질문 부터 답을 해야 될 것이다. 잡일은 많이 있을 것이다. 그렇다면 한달에 30만원돈 줘야 되는 사람을... 실력은 아마추어, 하는 일은 어차피 잡일 이걸 회사 시각에서 볼때

 

과연 할만한 가치가 있느냐, 대충 이런것이다. 만약 내가 회사 내에서 저런 상황에 처한다면, 알바 고용 안한다. 아는 후배나 그런 녀석들한테 돈 쪼끔 쥐어주고 시켜먹는게 더 싸게 먹힐 것이다.

이것 뿐만이 아니라 여러가지 측면에서 생각해보았다. (이것 때문에 안쓸려고 했었다~)

 

어느 하나 조건을 만족시킬 만한게 없었다. 보안문제도 그렇고..어차피 이력서를 쓰게 된것도 "심심해서" 이게 가장 클 지도 -_-;; 그때도 kgda에 들어갔다가 다른 사람들 이력서를 보면서 생각했다. "이렇게 많은 사람들 사이에서 나 자신을 팔때는 어떤식으로 공략을 해야 할까" 이런걸 고민하다가

 

위에를 누르니까 게임잡으로 갔다. 아이디는 옛날에 만들어 둬서 로긴을 하니 이력서 작성 방식이 바꼈으니 이력서를 작성하라는 것이다. -_-; 할짓도 없는데, 끽해야 10분 걸리겠지 하는 생각으로 쓰게 되었다. 그 이력서에서 자기소개서 부분을 여기에 올린다.

 

자기소개서라기 보다는 "포부"에 가깝다. -_- 생각을 한게 평범하게 "저는 어느곳에서 형제는 어떻게 어릴때는 저렇고....." 이런건 좋지 않다고 생각했다 그렇다고 해서 "전 혼자서도 왠만한 게임은 다 만들수 있습니다!!" 이런 초딩틱한 스타일은 싫었다.

 

완곡조절이 필요했다. 적당하게 강하면서도 "이런 생각을 하는 녀석이구나" 정도의 느낌...쓰고 나서도 무언가 좀 부족하다는 느낌을 많이 받았지만 꽤 걸렸다 쓰는데 -_-;; 그래서 걍 거기서 끝냈던게, 모니터링 하던 교찬이형 눈에 걸린것 같다. ㅎㅎ pmg에서는 내가 약한 부분만 하고 있다.

 

DB라던가 규약되어진 C언어 독점스타일, 프리젠테이션과 기획.....꼭 들키면 안되는게 들켜버린듯한 느낌이랄까? 나 보다 어린 사람들 앞에서 강의를 하거나 이야기를 하는 건 꽤 즐겨 하는 편이다. 동갑까지도 괜찬은데, 나보다 나이가 좀 높은 사람들, 즉 경어 및 존칭을 쓰는게 선택이 아니라

 

필수가 되는 상황은 꽤나 거북하다. 음 뭐랄까 좀 난해하다?? 느낌을 제대로 표현하지 못하겠다. 어쨌든 이런 부분을 긁고 있으니 내게는 좋은 기회가 아닐 수 없다. 공식적으로 나가기 전에 이런 곳에서 취약한 부분을 긁고 있다는 기회가 꽤 좋다. ^^;

 

아 그리고 이력서의 이름을 정할때, 굉장히 중요하게 생각했다. 하루에도 수십개씩 올라온다. 이력서가 -_-...경력자, 신입, 프리랜서 등.....엄청나게 많은 것들 중에서 내 이력서를 보고 싶게 만들어야 한다. 제목의 중요성은 이미 많이 알려저서 내가 다시 말할 필요는 없을 것 같다.

 

자 그렇다면 어떻게 제목을 정할까? 약간 강하게 해서 튀는 편이 좋을꺼라 생각했다. 역시 여기서도 완곡조절이 필요하다고 느꼈다. 약간 강하지만 튀는 느낌이나 거부감이 없는 정도..그리고 가장 중요하게 생각한건 내가 그 일에 대해서 알고 있다. 는 점을 제목에서 보여주기로 생각했다

 

생각해보자 "왠만한 게임은 다 만들어 봤습니다" 이런 제목이 과연 어필 할 수 있을까?

되려 찍힐 것이다. "게임 프로그래머로 지원합니다" 이런 제목도 어필 할 수 없다고 생각한다.

생각해 봐라. 아주 빈번히 일어나는 일인데 보통 Q&A 게시판에 한번 가보면 쉽게 알 수 있다.

 

특히 "데브피아" 같은 곳을 가보면 "질문있어요", "서버에 대한 질문이요" 이런 막연한 질문이 넘친다. 전자는 정말 초딩이라 욕하고 싶다. 여기는 지금 Q&A 게시판이다. 당연히 질문이 있다는 것 쯤은 무의식적으로라도 알게 된다. 그런데 제목에 "질문~~" 하면 그게 무슨 질문인지

 

글은 수십개 수백개까지도 올라온다. 저런 제목에는 답변해주기가 쉽지 않다. 답변하는 사람들이 시간이 남아 도는게 아니다. 자기 일 하다가 잠깐 보는 것이다. 그 잠깐에 시간동안 시선을 잡아야 한다. 대답은 간단하다. 제목으로 핵심을 비추어야 한다.

 

"서버에서 클라이언트의 패킷을 받지 못합니다", 제목 치고는 너무 길까?? 이 정도가 읽기 딱 좋은 것 같은데, 조금더 간소화 시키면 "클라이언트가 패킷을 받지 못합니다", "서버와 클라이언트가 주고 받을 수 없습니다" 등..

 

질문이 애매 하다면 "이미지 로드 중에 궁금한 것이 있습니다" 이 정도도 꽤 좋다고 생각한다.

목적은 감추어저 있다. "이미지 로드 중" 이라는 행동은 분명 있지만, 목적은 감추어저 있기 때문에

이미지 컨트롤을 아는 사람이라 하면 한번쯤 볼만 할 정도다.

 

이 처럼 내가 게임을 만드는 것에 대해서 조금 이나마 알고 있고, 그 중에서도 중요하게 생각하는게 무엇인지, 나의 생각을 나는 제목에 담아보았다. 제목과 자기소개서를 올리고 그만 자야겠다. ^-^

 

이 내용에 추가 및 기타는 리플을 달아주세요~~ 제가 쓴게 답은 아닙니다. 다만 제 입장에서 본 시각 일뿐 입니다.

 

제목 : 혼자서는 아무것도 못하는 게임프로그래머 입니다.

내용 : 혼자서는 게임을 만들수 없습니다. 이런 점을 잘 알고 있기 때문에 팀원들 간에 의사소통에 힘을 기울이고 있습니다. 뛰어난 기술력도 중요하겠지만 그 보다도 먼저 원활한 의사소통이 받처주어야 된다고 생각합니다. 제가 배워온 시간은 굉장히 짧습니다. 아직 인생에 1/3 조차도 다 살지 못했습니다.

 

그 만큼 앞으로 배울 시간이 많이 남아 있고, 또 하고자 하는 의지가 제게는 충분히 있습니다.
현재 고등학교를 다니고 있으면서, 학교와 제 자신의 공부, 두가지 모두 충실히 하고 있고 의사소통을 좀 더 원활하고 잘 할수있도록 학교 선생님과 많은 이야기를 합니다.

친구와 이야기 하면서 능력을 키우는 것 보다도 선생님과 이야기를 많이 하는 것이 더욱 도움이 되는 것 같았습니다. 물론 친구들과의 의사소통도 충분히 하고 있습니다. 제 개인적인 생각이지만 학교 안에서 다른 사람들 눈에 비친 제 모습의 평가는 꽤 괜찮다고 생각하고 있습니다.

그렇게 만들기 위해서 노력해왔고, 지금도 하고 있으며, 앞서 말씀드린바와 같이 앞으로도 계속 제 자신을 가꾸어 나가겠습니다. 현재는 고등학생 입장으로써 할 수 있는 일을 하고 싶습니다. 앞으로 몇개월만 있으면 졸업을 하게 되지만 현재는 고등학생, 학생에 입장에서 할 수 있는 일이 있다면 제게 언제든지 연락 주시기 바랍니다.

 

대학생이 되어도 학교와 일을 병행을 해야 되는 입장이 되어버리니까 현재 고등학생 입장에서나 나중에 대학생 입장에서나 크다 다르지 않을꺼라 생각됩니다.

저는 일을 배우고 싶습니다.  포트폴리올이 조금 더 멋지게 다듬어 지면 그때 올리도록 하겠습니다.

이런 글로 저를 평가 하기에는 부족할 것입니다. 저를 옆에 두시고 평가해 주시기 바랍니다.

칼루
나만의 강의 2004. 7. 16. 00:41
,

4.PlayStation 1(one) 해부학 - Mult Palette, It's Tim!!(4Bit & 8Bit)

Mult Palette, It's Tim!!


작성자 : kallru(신휘재)
E-Mail :
kallru@kornet.net

 

※이 문서는 제작자의 허락 없이 무단 배포 할 수 없음을 알려 드립니다.

 

- 목  차 -
14.Palette의 개념
15.Mult Palette
16.8Bit Tim분석
17.Mult Palette for 4Bit Tim
18.동적인 4Bit Palette
19.DrawImage
20.필자의 글

 

14.Palette의 개념

사실 이런건 다른 문서들을 보거나, 인터넷을 통해 좀 더 자세하게 배울 수 있을 것이다. 하지만 요즘 Palette를 사용하는 곳은 찾아 보기 힘들 정도로 많이 줄었다. 윈도우로 발전하면서, 대용량화되고 고속화 되어진 컴퓨터에 발전 때문에 더 이상 Palette의 필요성이 사라진 것 같다. Dos 시절에 프로그래머라면 분명 Palette에 빠삭하겠지만, 나 또한 Windows 프로그래머 시대라서 처음 이 Palette를 컨트롤 하는데 약간 난감한 부분도 없지 않아 있었던 것 같다. 순서라 하면 Palette를 알고 나서 16Bit나 24Bit처럼 그런식에 이미지 컨트롤을 배우는게 순서일지 모르겠지만 요즘은 시대가 많이 바뀌어서 오히려 거꾸로 되어진 것 같다.
 Palette는 무엇일까? 필자가 이 단어를 처음 알게 된건 아마 초등학교 3∼4학년쯤이 아닐까 생각된다. 우리는 그림을 그릴 때 Palette라는 곳에다가 사용하고자 하는 물감을 넣어둔 후에 붓으로 Palette에 있는 물감을 사용해서 그림을 그린다. 아크릴판 같은걸로 많이 사용했었는데.. 어쨌든 여기서 말하는 Palette도 그것과 똑같다. 과거 Dos시절에는 그림을 그리는 작업 자체가 상당한 일이였던 것 같다. 물론 지금도 꽤 큰 일이긴 하지만.... 어떤 그림에서 사용할 색을 정해서 그 색의 묶음을 Palette로 만들어 두고, 이것만을 가지고 그림을 그리는 것이다.
 앞의 강좌(16Bit,24Bit)에서는 이미지의 한 픽셀이 갖는 정보에 따라서 16Bit와 24Bit로 나뉘어 젔었다. RGB를 어떻게 표현하고 있는지, 그러한 정보들이 이미지 데이터에 있었지만, Palette를 사용하는 8Bit나 4Bit에서는 이미지 데이터는 단순히 Index에 불과하다. 다시 말해서 24Bit에서는 이미지 데이터 자체가 색의 정보를 담고 있었다. 하지만 Palette를 사용하는 이미지에서는 이미지 데이터가 색의 정보가 아니라 Palette의 Index라는 것이다.
 8Bit를 예로 들면 총 256가지의 색을 사용하는데, 이는 한 픽셀의 데이터 값이 0∼255 라는 것이다. 이것이 실제의 RGB값이 아닌 Palette의 번호라는 것이다. Palette에 0번부터 255번까지 색을 저장해둔다. 빨주노초파남보....등 조금 더 이해를 돕기 위해서 행렬로 보자.(표로 만들면 표를 안먹는 부분들이 많아서..)

[0번:빨강] [1번:노랑] [2번:파랑] [3번:초록] [4번:검정] [5번:보라] 등..
위에 표현한 색들은 예제일 뿐이

 

다. 실제로 저런 배열로 들어 있는 것이 아니니, 착각하지 말길 바라며 이렇게 Palette를 구성 해두고 내가 빨간색을 쓰고 싶다면 이미지 데이터에 0번이 들어가는 것이다.

0 1 2 2 3 4 0 1 2...-> 이미지 데이터를 뽑아서 얻는 이런 값들은 Palette의 Index라는 점을 잊지 말자.

그리고 Palette를 변경 했을 경우!! 아주 재미난 일이 일어난다. 위 예제와 같은 Palette를
[0번:보라] [1번:검정] [2번:초록] [3번:파랑] [4번:노랑] [5번:빨강]

이렇게 거꾸로 순서를 바꾸어 준다면? 이걸 그대로 0 1 2 2 3 4 0 1 2.. 이런 이미지 데이터에 적용해준다면 같은 그림이지만 색이 전혀 다른 그림이 나오게 되는 것이다.

 

15.Mult Palette
이제 Palette는 알겠다. 그렇다면

 PlayStation 1에서 사용하는 이 지긋지긋한 Tim에서 사용하는 Mult Palette라는 건 몰까? 말 그대로 여러개의 Palette를 가지고 있는 것이다. Palette를 사용하는 그림들은 아주 재미난 특성 하나가 존재한다. 위에서도 언급했다시피 바로 Palette를 바꾸면 그림은 같지만 색이 마구마구 변하는 것이다. 바로 이런 점을 이용해서 PlayStation에서는 효율적으로 그림들을 컨트롤 하고 있다. 이것이 바로 Tim에 진짜 힘이자 존재 이유일 것이다. 이 Mult Palette라는 것은 <Palette가 여러개 있다>는 이것 하나다. 어려운 건 하나도 없다. 아래에서 바로 이런 Palette의 특성을 이용한 Tim의 재미난 점들을 자세하게 서술하게 될 것이다.
 Tim에서 Palette는 최대 256개의 색상을 가질 수 있다. 1개의 색상은 RGB 그리고
Reserved, 이렇게 4개의 데이터로 구성되어 있다. 그렇다면 Tim에서 Palette가 차지하는 용량은 256*4=1,024 라는 계산이 나온다. 그리고 알아야 할 것이 8Bit에서는 256개의 색상 Palette를 가지지만 4Bit에서는 16개의 색상 Palette를 가지게 된다. 결국 Tim에서 할당해주는 Palette의 공간은 1,024인데 4Bit로 보자면 16*4=64 로 1,024에 16분의 1이 된다. 즉 64/1024 = 1/16 이라는 것이다. 바로 이곳에서 Mult Palette를 사용하는 것이다. 4Bit는 16개의 서로 다른 색상 테이블, 즉 각각 다 다른 16개의 Palette를 가질 수 있는 것이다. 16색상을 16개를 가지니 16*16=1,024로 계산이 성립된다.

 

16.8Bit Tim 분석
이제 Mult Palette까지 알았으니 8Bit를 분석해보자. 24Bit 만큼이나 쉽다. 먼저 우리가 해줘야 할 것은 8Bit Tim에서 Palette를 뽑아줘야 한다. 뽑기 싫으면 말고... Palette에 존재하는 색상 값은 15Bit BGR 이다. 이것은 전 강좌인 ConverBMP(16Bit)에서 비트마스킹을 이용해서 뽑아 냈었다. 이걸 응용하면 되는 것이다.


 Ptt = new RGBQUAD[256];

 Temp1 = (WORD *)PInfo.Data;
 for(i=0; i<256; i++)
 {
  b = (Temp1[i] >> 7) & 0xf8;
  g = (Temp1[i] >> 2) & 0xf8;
  r = (Temp1[i] << 3) & 0xf8;

 

  Ptt[i].rgbBlue = b;
  Ptt[i].rgbGreen = g;
  Ptt[i].rgbRed = r;
  Ptt[i].rgbReserved = 0;
 }


전 강좌와 마찬가지로 BYTE형으로 바꾸어서 읽고 쉬프트로 0∼31범위로 잡아준 후에 0xf8로 비트마스킹 하고 있다. 여기서 얻어낸 BGR을 Palette 변수에 넣어주고 있다.
 RGBQUAD 구조체는 내가 만든 것이 아니라 지원해주는 구조체이다. 아마 bmp format용으로 만들어진 구조체 일 것이다.
 이미지 데이터를 가공하는 건 똑같다. 먼저 이미지 데이터를 bmp처럼 역순으로 정렬한 뒤에 bmp Header을 쓴 후에 저장 할때 Ptt를 적절한 위치에 껴서 저장하면 된다.

 

 for(i=0; i<imgInfo.Height; i++)
 {
  BmpOffset = ((imgInfo.Height-1)-i) * (imgInfo.Width*2);
  for(j=0; j<imgInfo.Width*2; j++)
  {
   Temp2[BmpOffset2+j] = imgInfo.Data[BmpOffset+j];
  }            //bmp에 맞추어서 역순으로 정렬한다.
  BmpOffset2 = (imgInfo.Size)-BmpOffset;
 }

//==================== BMP Info 작성 ========================
 Image.Width = imgInfo.Width*2;
 Image.Height = imgInfo.Height;
 Image.Size = imgInfo.Size;
 
 Image.Data = new BYTE[imgInfo.Size];
 ZeroMemory(Image.Data,imgInfo.Size);
 memcpy(Image.Data,Temp2,imgInfo.Size);

 

이 소스만 보면 아래는 뻔하다는 걸

 

 알것이고, 다만 bmp로 저장할 때 Palette 정보가 어떻게 위치 되는지만을 언급 하겠다.

 

//Header 작성
WriteFile( Wfpt,&BMPFileHeader,sizeof(BITMAPFILEHEADER), &RWSize, NULL);
WriteFile( Wfpt,&BMPInfoHeader,sizeof(BITMAPINFOHEADER), &RWSize, NULL);

 

//Pallette Write
WriteFile( Wfpt, Ptt, 256*4, &RWSize, NULL);

  
//이미지 데이터 작성
WriteFile( Wfpt,Image.Data,Image.Size,&RWSize,NULL);

 

빨간색 부분이 작성한 Palette를 저장하는 것이다. bmp format은 header이 가장 먼저 오고 그 다음 Palette 정보가 온다. 이것이 다 온 뒤에 마지막으로 이미지 데이터가 오는 것이다. 이런 순서는 거의 모든 이미지 format들이 사용하고 있다. 물론 Tim도 이런 전체적인 순서는 다르지 않다.

 

17.Mult Palette for 4Bit Tim
드디어 16Bit, 그 이상에 난이도를 가지고 있는 4Bit Tim이다. Palette로 그림을 짤 경우, 하나의 아주 특이한 특성이 생기게 된다. 그것은 Palette를 교체하면 그림에 색상이 모두 교체한 Palette에 따라 바뀐다는 것인데, 바로 이걸 이용해서 아주 교모한 트릭을 사용한 format이 이 Tim이다. 위에서 4Bit는 최대 16개의 색상 Palette를 16개 가질 수 있다고 했다. 즉 색상 수로만 따지자면 256개를 사용 할 수 있는 것인데..이걸 이용하면 16장의 색이 다른 그림을 가질 수 있다는 것이다.
 첫장에 그림은 전체적으로 빨간색만을 사용하는 Palette를 쓰고 두 번째 장은 파란색, 세 번째 장은 초록색...이런식으로 16장의 그림을 "색이 다르게" 표현이 가능 하다는 것이다. 용량과 크기, 즉 메모리 점유는 같지만 16장의 색이 다른 그림을 표현하는게 가능 하다.

[예 : 같은 그림이지만 색이 모두 틀리다. 이렇게 Tim은 총 16장의 그림을 가질 수 있다]

 

이런 식으로 16장을 가진 다는 소리는 곧 16개의 Palette가 있다는 것인데, Palette를 잘 이해 했다면 이런건 그리 대단한게 아니라는 걸 알 수가 있다. Palette는 색상 정보 이고, 이미지 데이터는 색상 정보에 Index 값을 가지고 있는 구조 이다. 당연히 Palette를 교체하면 다른 색의 그림들을 얻을 수가 있다. 하지만 진짜 Tim은 바로 이 다음 부터라 할 수 있다.

 이런 구조 속에서 첫 번째장과 두 번째장의 그림이 다르다면?? 1개의 파일에 2∼3개의 그림이 존재한다면? 당연히 무압축이다. Palette가 가지고 있는 저런 교체되는 특징을 이용해서 1개의 그림 파일에 여러장의 그림파일을 위치 시키는 것이 가능하다. 물론 복잡한 그림이거나 하는건 힘들지도 모르겠지만, 어쨌든 여러장의 그림이 존재할 수가 있는 것이다.

 

<< 이것에 대한 강좌는 로츠님께서 쓰신 강좌를 그대로 인용하겠습니다. >>


<출처 : 한식구(http://hansicgu.hemosu.com)>
그림 크기 문제로, 링크를 걸어 두겠습니다.

링크

이런 Tim특징 중 중요하게 알아야 할 것은 서로 다른 그림을 하나에 데이터로 묶을 때 OR를 사용한다는 점이다. 이것은 나중에 다시 언급할 기회가 올 것 같다.
이제 소스를 보도록 하자.

 Ptt = new RGBQUAD[16];
 BYTE r,g,b;

 //RGB 값 뽑기 팔레트는 16BitBGR컬러로 저장되어 있다
 Temp1 = (WORD *)PInfo.Data;
 for(int i=0; i<16; i++)
 {
  b = (Temp1[i] >> 7) & 0xf8;
  g = (Temp1[i] >> 2) & 0xf8;
  r = (Temp1[i] << 3) & 0xf8;

  Ptt[i].rgbBlue = b;
  Ptt[i].rgbGreen = g;
  Ptt[i].rgbRed = r;
  Ptt[i].rgbReserved = 0;
 }
8Bit 분석할 때와 똑같다. 다만 for문을 16번 돈다는게 틀리다면 틀린 점이라 할 수 있다. 여기서 주목 해야될 점이 또 한가지 있다 그것은 Temp[i] 바로 이 부분인데, 이곳에 대가 16배수를 더해주면 다음 Palette를 가르킬 수 있다. Palette는 연속적으로 위치하고 있기 때문이다. 처음 0∼15 까지는 1번째 Palette가 되고 두 번째 16∼31 까지는 2번째 Palette가 되는 것이다. 이렇게 0∼15번까지 16개의 Palette가 위치되어 있으니 Temp[i+16의 배수]를 해준 다면 계속해서 다음 걸 지목 할 수 있을 것이다.

 

이제 해야 될 것은 이미지 데이터를 저장하는 일 인데 4Bit는 그리 쉽지만은 않다. 여기서 우리는 지긋지긋한 Big 방식과 Litte 인가 몬가 하는 그곳에 또 다시 부딧친것만 같다. 이건 내 추즉인데 Big 방식과 Litte인가 하는 그 것 때문에 4Bit 단위로 거꾸로 되어 있는 것 같다. 하지만 어째서 4Bit 단위로 그렇게 되어 있는지 또 모르겠다. 어쨌든 우리가 해야 되는 것은 거꾸로 되어 있는 것들을 제대로 읽어야 한다는 것이다. 아니 제대로 있는걸 거꾸로 해야 하는걸까?? 어쨌든 뒤집어 줘야지만 제대로된 값을 얻을 수가 있다.

 

 //4비트씩 자리가 바뀌여저 있기에 4비트씩 비트 마스킹을 한다.
 //0xf0 -> 1111 0000
 //0x0f -> 0000 1111
 BYTE Bit1,Bit2,Bit3;

 for(i=0; i<imgInfo.Size; i++)
 {
  Bit1 = imgInfo.Data[i] & 0xf0;
  Bit2 = imgInfo.Data[i] & 0x0f;
  Bit3 = (Bit2<<4) | (Bit1>>4);
  index4.push_back( Bit2 );  //나중에 사용
  index4.push_back( (Bit1>>4) ); //나중에 사용
  Temp2[i] = Bit3;
 }


우린 여기서 다시 비트마스킹을 사용해야 한다.
0000 0000 이런 1Byte에서 4Bit씩 끊어 오겠다면
1111 0000 A
0000 1111 B

이렇게 두가지로 비트마스킹 하면 각각에 상위 4Bit와 하위 4Bit를 끊어서 얻어 올 수 있다.
끊어서 얻어온 4Bit를 B를 상위로 끌어 올리고 A는 하위로 끌어 내리면 결국 4Bit씩 뒤집어서 저장 할 수 있다. 이 데이터를 가지고 이제 역순 정렬하고, bmp로 header짜고, 저장할 때에는 8Bit와 마찬가지로 저장 시켜주면 된다. 단! 이때 256 * 4가 아니라 16 * 4라는 점을 잊지 말자.

 

18.동적인 4Bit Palette
굳이 이런건 읽는 독자가 생각해서 하면 되지만, 4bit는 그 특성상 Palette를 바꾸어 주어야 할 필요가 있다. 그렇기 때문에 Palette를 읽어 오는 부분을 따로 만들어 두는 것이다. 그래서 때에 따라서 적절하게 바꾸어 주면 되겠다. TimCollection 에서와 같이 한 장의 4Bit 이미지에 있는 여러장으 그림을 볼 수 있도록 해주려면 Palette를 따로 읽는 루틴을 가저야 할 것이다.

 

void CTIM::PaletteBMP4()
{
//=========== 4 BPP Mode Palette 작성 =============

 WORD *Temp1;
 Ptt = new RGBQUAD[16];
 BYTE r,g,b;

 //RGB 값 뽑기 팔레트는 16BitBGR컬러로 저장되어 있다
 Temp1 = (WORD *)PInfo.Data;
 for(int i=0; i<16; i++)
 {
  b = (Temp1[i+NextNum] >> 7) & 0xf8;
  g = (Temp1[i+NextNum] >> 2) & 0xf8;
  r = (Temp1[i+NextNum] << 3) & 0xf8;

  Ptt[i].rgbBlue = b;
  Ptt[i].rgbGreen = g;
  Ptt[i].rgbRed = r;
  Ptt[i].rgbReserved = 0;
 }
}


그냥 함수화 했을 뿐이지 틀린게 없다. 다만 NextNum 이라는 변수가 하나 추가되었는데 위에서 말했다시피 16의 배수가 들어 가게 된다. 이 값에 따라서 여러개의 Palette를 읽어 들이게 되는 것이다.

 

19.DrawImaeg
여지껏 많은 짓거리들을 해 왔는데 이것들을 이제 화면에 출력해야 한다. bmp format 변환도 화면에 출력하기 위해서 처음 시도했었는데 이제는 화면 출력이 부수적인게 되버렸다.
bmp 출력은 어렵지 않다. 이미 역순정렬과 header도 모두 작성했기 때문에, 실제 메모리에는 tim과 bmp 모두가 올라와 있는 것이 된 거다. 때에따라 적절하게 사용하면 된다. dib bmp를 출력하는 것은 StretchDIBits 라는 함수가 제공된다. 다 알겠지만, hdc를 하나 만들어 두고 이곳에다가 모두 저장이 완료되면 이걸 고속으로 화면 hdc에 복사하는 방식을 사용한다. 게임쪽에선 플리핑이라 하는데, 이것 보다도 더 빠른 처리를 하는 것은 페이징 플리핑이라 하여 포인터만을 바꿔주는 거지만, 이런건 몰라도 되고 이 StretchDIBits를 통해서 16Bit와 24Bit 그림을 화면에 뿌릴 것이다.

 

BITMAPINFO Bitmapinfo;
ZeroMemory(&Bitmapinfo,sizeof(BITMAPINFO));
memcpy(&Bitmapinfo.bmiHeader,&BMPInfoHeader,40);
   StretchDIBits(hdc,POS_X,POS_Y,Image.Width,Image.Height,0,0,Image.Width,Image.Height ,(PBYTE)Image.Data,&Bitmapinfo,DIB_RGB_COLORS,SRCCOPY);

 

이런식이다 StretchDIBits이 함수에 대한 자세한건 msdn같은 곳에서 찾아 보길 바라며, 8bit와 4bit의 출력 방식을 보겠다. 이 두 녀석들은 직접 화면에 찍어주어야 한다. 처음 bmp 출력으로 Palette도 잡아주고, 출력을 시도했었는데 색도 제대로 표현 안될뿐더러 그때그때 상황에 따라 색이 변질 되거나 하는 일이 발생했었다. 그래서 느리지만 직접 하나씩 찍어주기로 하자. 물론 빠른 처리를 해야된다면 좀 더 좋은 방법을 생각해 보길 바란다.

 

int i,j;
WORD Offset=0;
list<WORD>::iterator iter=index4.begin();
for(i=0; i<Image.Height;i++)
{
 for(j=0; j<Image.Width;j++,iter++)
 {
  switch(TIMHeader.BPPMode)
  {
  case 0x08: //4bit
   ::SetPixel(hdc,j+POS_X,i+POS_Y,RGB(Ptt[*iter].rgbRed,
    Ptt[*iter].rgbGreen, Ptt[*iter].rgbBlue));
   break;
  case 0x09: //8bit
  ::SetPixel(hdc,j+POS_X,i+POS_Y,RGB(Ptt[imgInfo.Data[Offset+j]].rgbRed,
  Ptt[imgInfo.Data[Offset+j]].rgbGreen,Ptt[imgInfo.Data[Offset+j]].rgbBlue));
  break;
  }
 }
 Offset += Image.Width;
}


SetPixel 이라는 함수를 이용하는 것이다. 앞서 말했듯이 4,8Bit는 이미지 데이터 자체가 Index값이라 그 데이터 값을 읽어온 Palette에 대입하고 그걸로 얻어진 값을 RGB 함수로 색체와 시키면 된다. 8Bit는 그냥 가저와서 찍어주면 되므로 별다른게 없지만, 문제는 4Bit이다 각각에 Index 값이 4bit로 되어있기 때문이다. 여기서 필자는 알고리즘<List>를 사용해서 아까 주석문으로 "나중에 사용" 이라고 붙여둔 부분이 있을 것이다. STL을 사용한 것으로 쪼개둔 4Bit를 하나씩 모두 저장해둔 것이다. 이 부분에 대해서는 더 이상 언급하지 않겠다 필자는 List를 이용해서 4Bit 값을 하나씩 저장 시켜둔 것이다. 여기서 필요한건 4Bit 값을 하나씩, 얻어 와야 한다는 것이다. SetPixel 부분에서 다시 비트마스킹 해서 얻어와도 큰 상관은 없겠지만...그렇게 까지 한다

 

면 상당히 느려질 듯 싶어서 필자는 알고리즘<List>를 사용한 것이니, 이 부분은 자신에 능력껏 해결해야 할 듯 싶다. 이것까지 완성 되면 이제 한달여간 쓴 강좌가 끝이 난다.

 

20.필자의 글
이제 다 쓴 것 같습니다. 처음 시작할 때 "이것 까지 써야지" 라고 생각했던 부분입니다. 부족한게 많고, 불필요한 말이 많아서 강좌에 분량이 좀 크지만....강좌를 다 쓰고 이런 말까지 덧붙이기는 처음이랍니다. 그래도 꼭 한번 해보고 싶었습니다. 이걸로 강좌를 더 이상 쓰지 않겠다는게 아니라 이제 막 일단락 된거라 생각됩니다. 마지막 강좌여서 그런지 그림파일도 많이 첨부가 되었고, Palette 강좌 때문에 색상도 많이 쓰여지게 되었습니다. 어쨌든 끝을 낼 수 있어서 기쁘고, 제가 여지껏 쓴 강좌를 배포 할때에는 먼저 제게 E-Mail로 또는 다른 연락처로 제게 물어주시기 바랍니다. 거절 하는 일은 없겠지만 최소한의 예의라 생각됩니다. 힘들게 쓴 것이 아무런 동의 없이, 이곳저곳에 날라 다닌다는건 그리 기분 좋은 일이 아니기 때문 이죠. 또 항상 출처 표기는 정확하게 해야 된다는 점도 잊지 말아 주시길 바라며, Tim 강좌는 여기서 마치도록 하겠습니다.

 

Tim을 분석하고 있는 사람, 하려는 사람, Tim에 대한 실질적인 프로그램 레퍼런스!
나, 다음에 사람을 위해서 강좌는 계속 됩니다.


 
칼루
나만의 강의 2004. 5. 9. 17:24
,

[펌] &lt;펌&gt; 버그 없는 깨끗한 프로그램 만들기

버그 없는 깨끗한 프로그램 만들기  2002년 05월 12일 | 03시 41분

필자에게 프로그래밍이 무엇이냐고 묻는다면 프로그래밍이란 “버그와의 끝없는 싸움”이라고 대답하고 싶습니다. 필자가 처음 프로그래밍을 접해본 것은 중학교 3학년 때인 1984년이었습니다. 친구 집에 놀러갔다가 접한 SPC-1000에서 베이직으로 간단하게 계산기를 만들어본 것이 처음이었습니다. 그 컴퓨터란 물건이 얼마나 부럽던지 반년 동안 아버지를 졸라서 고등학교 1학년 때 애플 II 컴퓨터를 샀고 몇몇 컴퓨터 잡지를 사서 소스를 아무 생각없이 입력한 것 말고는 입시 준비(?)에 시달리느라 제대로 프로그래밍을 해본 적은 없었습니다. 그러다가 대학에 들어와서 포트란, 파스칼, C등을 배우면서 좀더 본격적인 프로그래밍을 시작하게 되었습니다. 그 시절을 돌이켜보면 참 어떻게 그렇게 무식하게 (?) 프로그래밍을 할 수 있었는지 대단하다는 생각이 간혹 들기도 합니다.

그 시절을 잠깐 회상하자면 일단 무턱대고 컴퓨터 앞에 앉아서 프로그래밍을 시작하지만 넘치는 의욕에 비해 실력이 따르질 않아서 쉬운 숙제에도 쩔쩔매며 간신히 코드를 만들었습니다. 컴파일하면 에러가 잔뜩 !!. 간신히 에러를 잡고 나서 실행하면 프로그램이 죽던지 엉뚱한 결과가 나오고... 그러면 이제부터 디버깅이 시작됩니다. 일단 의심이 가는 곳을 대충 고친 다음에 먼저 돌려보고 안 되면 다시 의심가는 곳을 고치고... 코드가 누더기가 된 다음에야 간신히 코드는 돌아가고 원하는 결과가 나오기는 하는데 왜 동작하는지 본인도 알 수 없고...

아마 대부분의 프로그래머들이 비슷한 과정을 거쳤거나 앞으로 거치리라 생각됩니다. 100줄 이상의 프로그램이라면 프로그램에 버그가 없을 수는 없습니다 (아니 적어도 컴파일 에러라도 있습니다). 그렇기 때문에 좋은 프로그래머가 되려면 버그가 적은 프로그램을 작성할 수 있거나 버그를 빨리 발견할 줄 알아야 합니다. 이 글에서는 여러분들이 이러한 목표를 달성하는데 도움이 될 수 있도록 먼저 좋은 코딩 습관에 대해 살펴보겠습니다. 그리고나서 다음으로 실례를 중심으로 자주 접하게 되는 버그 상황과 디버깅 팁에 대해 알아보도록 하겠습니다. 참고로 여기서는 비주얼 C++을 바탕으로 설명을 진행하겠습니다.

1. 바람직한 코딩 습관

버그 없는 프로그램을 작성하는 가장 좋은 방법은 바람직한 코딩 습관을 몸에 배게 하는 것입니다. 저도 지금 생각해보면 처음 프로그래밍을 배울 때 누군가 옆에서 이러한 것에 대해 이야기해주는 사람이 있었다면 고생을 좀 덜하지 않았을까 하는 생각을 간혹 하곤 합니다. 물론 자신이 직접 깨닫는 것만큼 확실히 배우는 것은 아니고 그 당시 그런 조언을 이해할만한 수준이 아니었기 때문에 그런 것들을 들었다 해도 얼마나 습득할 수 있었을지 의문이긴 합니다만. 참 이런 이야기를 하다보니 제가 이제는 무슨 완벽한 프로그래머가 된 것처럼 되어버렸는데 저도 이런 코딩 습관을 확립하기 위해 지금도 노력하고 있는 중입니다.

자 그럼 제가 생각하는 바람직한 코딩 습관에 대해 하나씩 알아보도록 하겠습니다. 이제 읽어보면 알겠지만 여기에 무슨 눈이 번쩍 뜨이는 그런 이야기들이 있는 것이 아닙니다. 항상 그렇지만 모든 성공은 작은 습관과 집중력으로부터 비롯합니다.

1> 사용하는 프로그래밍 언어를 제대로 이해하자.

이 것은 초보 프로그래머에게는 아주 중요한 사항입니다. 프로그래밍을 처음 배우는 사람이라면 그 개념을 제대로 이해하지 못하는 경우가 허다합니다. 특히 C 언어를 배우는 사람이라면 포인터의 개념을 제대로 이해하지 못하는 경우가 많고 C++/자바 등의 언어를 배우면 클래스의 상속(Inheritance)이나 가상 함수(Virtual function)와 같은 개념을 이해하지 못하는 경우가 허다합니다. 이런 것을 이해하지 못하고 일단 프로그래밍에 뛰어들게 되면 당연히 많은 시간을 소비하기 마련이고 결과물을 만들어내지 못하는 경우도 많습니다. 아무리 시간이 없어도 한걸음 한걸음 배워나가는 것이 결국에는 시간을 단축하기 마련입니다.

프로그래밍 서적을 보고 공부할 때도 모니터 앞에 앉아서 지금 보고 있는 부분에 대해 실습을 해보며 공부하는 것이 최선의 방법입니다. 그렇게 하면 보다 더 이해하기가 쉽고 개발환경의 사용법에 대해서도 배워볼 수 있기 때문입니다. 그렇기 때문에 초보 프로그래머라면 프로그래밍 언어 관련 서적을 고를 때 예제 코드가 많이 있는지 또한 따라하기식의 구성이 잘 되어있는지를 살펴보는 것이 중요합니다. 즉 프로그래밍 언어는 머리로 이해하는 것도 중요하지만 손으로도 익혀야 한다는 것입니다.

top

2> 프로그래밍 중에는 프로그래밍에만 집중하자.

이 것은 초보 프로그래머 뿐만 아니라 이 시대의 모든 프로그래머에게 해당되는 이야기라고 할 수 있는데 한마디로 자신의 일에 대한 집중력을 키우라는 이야기입니다. 요즘 웬만한 컴퓨터는 인터넷과 연결되어 있습니다. 인터넷 매체와 메일, 메신저 등이 워낙 발달해 있어서 한 시간이라도 중단 없이 일한다는 것이 쉬운 일이 아닙니다. 이런 상황은 사실 프로그래머 뿐만 아니라 컴퓨터를 사용하는 모든 사람들에게 해당된다고 할 수 있을 것 같습니다. 다음과 같은 비슷한 경험이 있는지 한번 생각해봅시다.

코딩이 좀 막히면 머리를 식히기 위해 인터넷의 바다에서 뛰어들고 코딩이 잘 되어도 기분이 좋아서 인터넷의 바다에 뛰어들고... 또 요즘 메일 클라이언트는 모두 알림 기능이 있어서 메일이 도착하면 이를 알려주게 되어있습니다. 메신저는 어떠한가요 ? 툭하면 친구로부터 채팅 요청이 들어오고... 수다 떠느라 시간가는 줄 모르고...

의식적으로 노력을 해서 이러한 중단이 없도록 해야 합니다. 인터넷의 바다에 뛰어드는 시간대를 정해두는 것이 제일 좋은 것 같습니다. 점심 시간 직후라든가 하는 식으로 말입니다. 메일의 경우에는 새 메일이 도착했다는 알림이 와도 바쁜 경우라면 일단 그 일을 끝낸 후에 보는 식으로 한번 바꿔보기 바랍니다. 메일의 경우 같이 일하는 사람들간 의사소통 수단이기도 하기 때문에 알림 기능 자체를 꺼버리는 것은 좀 무리가 있는 듯합니다. 메신저 같은 경우는 집중이 필요한 시간에는 아예 로그아웃해버리는 것입니다. 아니면 약간 치사(?)하지만 상태를 “자리 비움” 혹은 “다른 용무 중” 같은 걸로 해두세요.

약간 이야기가 옆길로 새는 것 같긴 하지만 집중력에 관해 얼마 전에 제가 읽은 글이 있어서 참고로 여러분들께 소개해 드리고자 합니다. (“고도원의 아침편지”란 사이트에서 읽은 글입니다.)

밥을 먹을 때에는 밥먹는 일에 집중하고
청소할 때에는 온전히 청소하는 행위만 있어야 합니다.
그렇게 생각하고 말하고 행동하는 것을
달리 말하면, 집중력 또는 통일성이라고 합니다.
이 집중하는 태도와 노력을 통해
우리는 스스로 정화되기도 하고
안정되기도 하며
또 문제의 본질을 통찰하는
힘을 얻기도 합니다.


- 도법스님의 <<내가 본 부처>> 중에서

프로그래밍을 할 때는 프로그래밍에만 집중합시다 !! 인터넷으로 인한 이러한 중단이외에도 회사 생활을 막 시작한 초보 프로그래머의 경우에는 다음과 같은 것을 조심해야 합니다. 긴 업무 시간으로 인한 집중력 상실입니다. 이건 사실 개인의 힘만으로 해결할 수 있는 문제는 아닙니다만 아주 한국적인 근무 환경 하에서는 늦게 퇴근하는 것이 하나의 미덕입니다. 따라서 오랜 시간을 회사에서 보내려니 업무의 강도가 느슨해지기 마련입니다. 하루 근무시간을 8시간으로 생각하지 않고 10시간에서 12시간으로 생각하니까 할 일이 있어도 “저녁에 하지 뭐!“ 이런 생각을 갖게 되고 커피마시고 담배피우며 잡담하느라 보내는 시간이 더 많아지게 되는 것입니다. 사실 이는 매니저 역할을 맡고 있는 사람이 해결해야할 문제입니다만 이런 상황으로 인해 집중력을 잃는 일이 발생할 수도 있다는 점을 알아두고 자신이 이런 증상을 보이거든 회사를 옮기던가(?) 자신을 채찍질하여 다시 집중력을 되찾기 바랍니다.

3> 주석을 많이 달자

주석을 많이 다는 것도 굉장히 중요합니다. 아주 복잡하고 거창하게 코드 흐름도를 그리라는 이야기가 아닙니다. 소스 코드 중간 중간에 설명을 자세히 달아놓으라는 이야기입니다. 그것만으로도 다른 사람이나 코드의 원저자 자신이 나중에 소스를 볼 때 큰 도움을 얻을 수 있습니다. 제 아무리 자기가 작성한 코드라고 해도 복잡하고 사연 많은 코드의 세부적인 내용은 몇 개월만 지나도 잊어버리기 십상이기 때문입니다.

귀찮아서 주석을 안 다는 사람들도 많습니다. 심지어 주석이 코드의 미관을 해친다는 이색적인 주장(?)을 펼치며 주석달기를 거부하는 사람도 본 적 있었습니다. 오 마이 갓 !! 주석을 다는 일은 습관이 되면 그리 어려운 일이 아닙니다. 또 주석을 달면서 작성하는 소스의 구성이나 흐름이 보다 더 명확해질 수도 있습니다. 예를 들어 C/C++이나 자바로 코딩을 하는 중이라면 먼저 //부터 입력하고 밑에서 할 일을 간단히 적고 시작하면 자신이 해야할 일이 좀더 명확해지고 나중에 주석을 보고 코드를 기억하거나 이해하기도 쉽기 때문입니다.

소스 파일을 하나 새로 만들면 그 앞에 다음과 같은 식으로 주석을 달기 바랍니다.

// -------------------------------------------------------// 파일이름 : NewModule.cpp// 설명 : 전체 프로그램에서의 이 소스 파일의 역할을 기술합니다. // 노트 : 기억할만한 점이 있으면 기록합니다. // 히스토리 : 생성 -           한기용, 2002.03.31//            Abc 함수추가 -  두기용, 2002.04.01// -------------------------------------------------------

함수의 경우에는 함수마다 앞에 다음과 같은 함수 주석을 답니다.

// -------------------------------------------------------// 함수이름 : Abc// 설명 : 이 함수의 역할에 대해 기록합니다.// 노트 : 기억할만한 점이 있으면 기록합니다. // 인자 : 인자를 하나씩 설명합니다. 인자의 값을 누가 채워주는지도 명시합니다.//       [IN] int nCount  //       [IN] char *pString//       [OUT] int *pResultCount// 리턴값 : 리턴값의 타입과 의미에 대해 설명합니다.// -------------------------------------------------------

번거롭게 느껴질지 모르지만 한번 몸에 배면 아주 좋은 특히 같이 일하는 사람들이 좋아하는 습관이란 점을 분명히 기억해두기 바랍니다. 문서화를 잘 하는 사람들은 어디를 가든 사랑(?)받습니다.

4> 새로 작성한 코드는 항상 디버거로 따라가 보자

처음 작성했거나 잘 동작하던 코드를 수정한 경우라면 컴파일이 제대로 된 후에 한번 디버거로 흐름을 따라가 보면서 생각하는 대로 동작하는지 살펴보는 것이 아주아주 좋습니다. 좀 번거롭게 생각되어서 이런 이야기를 그냥 듣고 넘길 수도 있는데 이를 습관으로 만들면 여러모로 오히려 편리합니다.

그냥 일단 돌려보고 제대로 동작하지 않으면 디버거로 따라가 보는 전략을 택할 수도 있는데 그것보다는 처음 한번은 일단 디버거로 따라가 보는 것이 여러모로 좋습니다. 디버거로 따라가 보면 결과에 나타나지 않는 에러들도 찾아낼 가능성이 있고 다른 아이디어가 나올 가능성도 더 높기 때문입니다.

>

top

5> 테스트 코드를 만들자

훌륭한 프로그래머라면 누구나 버그없는 빠르고 깔끔한 프로그램을 만드는 사람이라고 생각할 것입니다. 하지만 앞서 제가 언급한 것처럼 복잡한 프로그램을 작성하다보면 버그 없는 프로그램을 작성하는 것은 거의 불가능한 일입니다. 그렇기 때문에 제가 생각하는 좋은 프로그래머의 요건은 버그를 어떻게 빨리 발견해서 없애느냐에 달려있다고 생각합니다. 참고문헌 3을 보면 “Debugging the Development Process”라는 책이 언급되어 있습니다. 이 책은 제가 아주 감명(?)깊게 읽은 책으로 제대로 된 개발을 하는 방법론에 대해 다루고 있습니다. 시간이 된다면 한번 꼭 읽어보기를 권합니다. 이 책에서는 버그를 발견하고 이를 없앨 때마다 항상 다음 두 가지를 생각해보기를 권하고 있습니다.

  • 어떻게 했으면 이 버그를 내가 미연에 방지할 수 있었을까 ?
  • 어떻게 했으면 이 버그를 내가 아주 쉽게 찾아낼 수 있었을까 ?

    이를 좀더 확장해서 생각하면 코드를 작성할 때부터 이걸 어떻게 테스트를 할 것인지 염두에 항상 두고 있어야 한다는 말입니다. 그래서 저 같은 경우는 프로그래머의 수준을 측정할 때 한 가지 척도로 그 사람이 테스트 프로그램을 작성하는지를 일단 봅니다. 자신의 코드를 테스트할 프로그램을 별도로 작성할 정도의 사람이면 일단 기본기가 아주 잘 되어 있는 사람이라고 볼 수 있기 때문입니다.

    코드에 따라서는 테스트하는 것이 아주 힘든 경우도 있을 수 있습니다. 하지만 그걸 핑계로 테스트를 건너뛰지는 말기 바랍니다. 테스트할 방법을 계속 생각해보면 결국 어떻게든 그 방법이 떠오르게 되어 있습니다. 테스트하기 힘든 상황에도 그 방법을 생각해낼 정도의 사람이라면 아주 긍정적인 사람으로 인식될 것입니다.

    결론적으로 무슨 코드를 만들던지 테스트할 방법을 생각하기 바랍니다. 프로그램 수행의 결과로 파일을 만들어내는 프로그램이라면 그 파일의 내용이 맞는지 체크하는 프로그램을 따로 만들어 볼 수도 있을 것이고 뭔가를 수행해서 화면에 출력하는 간단한 프로그램이라면 그 부분을 별도의 함수로 만들어서 다양한 입력을 주어서 올바른 출력이 나오는지 확인해볼 수 있을 것입니다.

    또 실행이 오래 걸리는 프로그램이라면 중간 중간마다 실행 상태를 파일등에 기록해두도록 하는 것도 아주 좋습니다. 이런 용도로 사용되는 파일을 흔히 로그 (log) 파일이라고 합니다. 이때 그 시각도 같이 기록해두면 도움이 많이 될 것입니다.

    6> 생각하는 프로그래머가 되자

    무슨 코드를 작성하던지 어떻게 할 것인지 먼저 생각하는 습관을 갖기 바랍니다. 그리고 프로그램이 정상적으로 동작하고 원하는 결과를 낸다면 이에 만족하지 말고 더 빠르고 간단하게 처리하도록 개선할 방법이 없는지 자꾸 생각해봐야 합니다. 그 당시에야 일단 실력이 모자라고 개선하는데 시간이 걸리니까 답답하게 여겨질지 모르지만 결국에는 프로그래밍 실력의 발전에 가속이 붙을 것입니다.

    사용자 인터페이스 프로그램을 만들고 있는 중이라면 사용자 입장에서 더 편리하게 사용할 수 있는 방법을 자꾸 생각해봐야 합니다. 물론 이 과정은 끝이 없는 과정이 될 수도 있기 때문에 뭔가 데드라인이 있는 일을 작업 중이라면 적정선에서 타협을 봐야 합니다. 이러한 타협은 회사의 운명이 걸린 상용 프로그램을 만들 때는 아주 중요합니다. 자꾸 개선하다가 오히려 에러를 더 낼 수도 있도 그로 인해 결과적으로 주어진 시간 내에 작업을 못 끝낼 수도 있기 때문입니다.

    요약하자면 무슨 프로그래밍을 하건 간에 먼저 생각하는 습관을 들이라는 것입니다. 그리고 결과로 만들어지는 코드의 질을 높이기 위해 노력하라는 것입니다. 이런 자세가 습관이 된다면 자신이 만들어낸 코드에 대한 안정성과 높은 성능을 보장해줄 것입니다. 이는 프로 프로그래머가 가져야 할 의식이며 이것이 바탕이 된다면 다른 사람들도 모두 여러분을 믿을만한 사람이라고 높게 평가해줄 것임에 틀림없습니다. 사실 필자도 프로그래밍을 배울 때 이렇게 하지 못했습니다. 프로그래밍을 오래 하다 보니까 그런 자세를 처음부터 갖는 것이 중요하다는 생각이 든 것입니다.

    2. 예로 살펴보는 버그와 디버깅 팁

    이제부터 실질적으로 코드를 통해 자주 접하게 되는 버그의 유형에 대해 예를 들어 알아보기로 하겠습니다.

    1> 부호 숫자 타입과 무부호 숫자 타입

    C/C++에는 부호 숫자 타입과 무부호 숫자 타입이 다음과 같이 존재합니다.

    타입 크기부호 숫자 타입 크기무부호 숫자 타입 크기
    8비트
    char (-128 ~ 127)
    unsigned char (0 ~ 255)
    16비트
    short (-32,768 ~ 32,767)
    unsigned short (0 ~ 65,535)
    32비트
    int (-2,147,483,648 ~ 2,147,483,647)
    unsigned int (0 ~ 4,294,967,295)
    32비트
    long (-2,147,483,648 ~ 2,147,483,647)
    unsigned long (0 ~ 4,294,967,295)

    비주얼 C++에서 사실 int와 long은 모두 32비트라는 점에 유의하기 바랍니다. 초보 프로그래머들이 많이 실수하는 분야 중의 하나가 바로 부적절한 정수 타입을 사용하는 것입니다.

  • 먼저 사용되는 데이터 값의 범위를 확인하고 그에 맞는 변수를 선택해야 합니다. 위의 표를 보고 적절한 변수를 선택하면 됩니다. 위의 표에는 없지만 사실 __int64라고 해서 64비트 짜리 정수 타입도 있습니다. 사실 대부분의 경우 int로 충분합니다.
  • 만일 사용되는 데이터의 값이 음수가 될 수 없는 것이 분명하면 무부호 정수 타입을 사용하기 바랍니다. 예를 들어 int 대신에 unsigned int를 사용하란 이야기입니다.
  • 그런데 무부호 정수 타입을 사용할 경우에는 비교 연산시에 아주 주의해야 합니다. 예를 들어 다음과 같은 코드를 보면
      unsigned int dwNumber1 = 100, dwNumber2 = 200;  if (dwNumber1 - dwNumber2 > 0)    AfxMessageBox("dwNumber1 is larger");  else    AfxMessageBox("dwNumber2 is larger");

    당연히 if 연산이 거짓이 되어 두 번째 메시지 박스가 출력될 것 같지만 그렇지 않습니다. 이 if 연산은 참이 됩니다. 무부호 정수간의 연산 결과는 다시 무부호 정수가 됩니다. 무부호 정수에는 말그대로 음수가 없습니다. 따라서 이 연산은 항상 참이 됩니다. 위의 코드는 다음과 같은 식으로 변경해야 합니다.

      if (dwNumber1 > dwNumber2)    AfxMessageBox("dwNumber1 is larger");  else    AfxMessageBox("dwNumber2 is larger");

    즉 무부호 정수끼리 뺄셈을 하는 경우에는 아주 조심해야 한다는 것입니다.

    2> 포인터 사용하기

    C/C++에 처음 입문한 프로그래머들이 가장 어려움을 느끼는 영역 중의 하나가 바로 포인터라는 개념을 익히는 것입니다. 포인터는 메모리 주소를 가리키는 변수입니다. 여기에 메모리를 할당해서 유효한 메모리 주소를 갖게 하기도 하고 다른 변수의 주소를 가리키게 하기도 합니다. 즉 포인터 변수는 선언한 후에 바로 사용하면 안 되고 무엇인가가 유효한 메모리 영역을 가리키게 만들어야만 한다는 것입니다. 전산을 4년 전공하고 회사에 막 들어온 사람들 중 상당수가 이를 제대로 이해하지 못한 상태임을 여러 번 보았습니다. 그 중의 한 예는 다음과 같습니다.

      void DoSomething(char *lpstrSource)  {    char *lpstrString;    strcpy(lpstrString, lpstrSource);    ...  }

    위의 코드를 보면 pString이란 포인터 변수를 선언한 후에 그걸 그대로 strcpy라는 함수에 사용하고 있습니다. 이 상태에서 pString이란 변수는 메모리의 아무 영역이나 가리키고 있습니다. 여기에다가 인자로 넘어온 pSource가 가리키는 문자열을 복사하려고 하면 당연히 프로그램이 뻗어버립니다. 이런 코드가 나오는 이유는 포인터라는 것의 개념을 이해하지 못했기 때문입니다. 그런 상태에서 strcpy 함수의 원형을 보고 그대로 변수를 선언하고 인자로 지정한 것입니다. 올바른 코드는 다음과 같습니다. 포인터에 메모리를 할당한 다음에 이 것이 제대로 할당된 경우에만 복사를 시도하는 식으로 변경해야 합니다.

      void DoSomething(char *lpstrSource)  {    char *lpstrString;    lpstrString = (char *)malloc(strlen(lpstrSource)+1);    if (lpstrString)      strcpy(lpstrString, lpstrSource);    ...  }

    다시 정리하자면 포인터란 메모리 영역을 가리키는 변수이기 때문에 무엇인가 유효한 영역을 가리키도록 초기화해야만 사용할 수 있습니다.


    참고 1. 메모리 할당

    컴퓨터 세계의 함수 중에는 쌍으로 사용되는 것이 무지하게 많습니다. 대표적인 것이 바로 메모리 할당을 할 때 사용되는 malloc 함수와 더 이상 사용할 일이 없을 때 이를 운영체제에 반환해주는 free 함수입니다. C++에는 new와 delete가 있지요. 이것들의 쌍이 제대로 맞지 않으면 메모리가 조금씩 조금씩 부족해지다가 결국에는 바닥나게 되어 있습니다.

    그런데 화장실 들어갈 때와 나갈 때 마음이 다르다고 처음엔 메모리가 필요하니까 할당해서 사용하지만 사후처리를 잊기 십상입니다. 뭐 간단한 프로그램에서야 메모리할당과 반환이 그다지 복잡하지 않지만 정말로 복잡한 프로그램에서는 이게 쉽지 않습니다. 서버 프로그램의 경우 아주 작은 양의 메모리가 반환되지 않는 경우에는 프로그램이 한두달 돌아야 그게 밝혀지는 경우도 있습니다.

    그래서 자바(Java)와 닷넷(.NET)에서는 이런 메모리 반환의 책임을 프로그래머에게 넘기지 않고 시스템 레벨에서 처리합니다. 즉 사용되지 않는 메모리가 있으면 알아서 반환시켜버리는 것입니다. 이를 가비지 컬렉션(Garbage Collection)이라고 부릅니다. 프로그래머 입장에서 두손 들고 환영할 일이지요. 하지만 이 가비지 컬렉션을 해주는 프로세스가 주기적으로 동작해야 하고 메모리가 바로 반환되는 것이 아니라 시간이 걸리기 때문에 프로그램의 실행 속도가 전체적으로 좀 느려지게 됩니다.


    3> 함수의 리턴값 체크

    초보 프로그래머이건 노련한 프로그래머이건 간에 또 많이 하는 실수가 바로 함수의 리턴값을 체크하지 않고 그대로 코드를 작성하는 경우입니다. 리턴 값을 체크하려면 if 문이 들어가야 하고 이게 좀 귀찮은 일입니다. 또한 이게 많아지면 코드를 한눈에 보기도 힘들어집니다. 그래서 많은 프로그래머들이 별일 있겠냐 하는 마음에 함수 리턴값 체크를 하지 않습니다. 예를 들어 파일 I/O를 한다고 하면 다음의 코드처럼 리턴 값을 체크하지 않는 경우가 허다합니다.

      // strFilePath가 읽어들이고자 하는 파일의 경로를 가리킨다고 하자.  CFile file;  char strHeader[256];  file.Open(strFilePath, CFile::modeRead);  file.Read(strHeader, 255);

    이런 코드는 strFilePath가 가리키는 파일이 없거나 파일은 있지만 파일의 크기가 부족한 경우에 에러를 내게 됩니다. 사용된 함수의 리턴 값을 모두 체크하도록 코드를 수정하면 다음과 같습니다.

      // strFilePath가 읽어들이고자 하는 파일의 경로를 가리킨다고 하자.  CFile file;  char strHeader[256];  bool bError = FALSE;  // CFile::Open 함수는 오픈이 성공하면 TRUE를 리턴한다.  if (file.Open(strFilePath, CFile::modeRead))  {    // CFile::Read 함수는 읽어들인 바이트수를 리턴한다.    if (file.Read(strHeader, 255) != 255)       bError = TRUE;  }  else    bError = TRUE;  if (bError) // 에러가 있으면  {    CString strError;    strError.Format("에러가 발생했습니다 - %d", GetLastError());    AfxMessageBox(strError);  }

    수정된 코드를 수정전의 코드와 비교하면 아마도 조금 더 코드의 흐름을 이해하기 힘들다는 느낌이 들 것입니다. 하지만 대신에 훨씬 더 코드가 안정적으로 동작하게 됩니다. 참고로 수정된 코드를 보면 에러가 발생했을 때 GetLastError 함수를 부르는데 이 함수는 윈도우 운영체제가 제공해주는 함수로 방금 발생한 에러에 해당하는 에러코드를 리턴해주는 역할을 수행합니다. 이 값에 해당하는 설명 문자열을 보고 싶다면 비주얼 C++의 도구(Tools) 메뉴의 오류 조회(Error Lookup) 명령을 실행한 다음에 거기에 에러코드를 입력해보면 됩니다. 다음은 “오류 조회“ 다이얼로그의 실행 화면입니다.


    < 그림 1. "오류 조회(Error Lookup)" 다이얼로그의 실행 화면 >

    가끔 테스트 프로그램을 작성하거나 시간이 없을 경우 방금 코드처럼 리턴 값을 일일이 체크하는 것을 생략하는 사람들을 많이 봤습니다. 코드를 작성하는 당시에는 나중에 제대로 검사하게 고쳐야지 하지만 그럴 만큼 부지런한 사람은 드뭅니다. 수정할 기회가 언제 올지 알 수 없기 때문에 무슨 프로그램을 짜던지 항상 최선을 다하는 것이 좋습니다. 즉 되도록이면 “나중에 고치지”라는 생각은 접어 두시기 바랍니다.


    참고 2. try/catch

    함수의 리턴값 체크가 번거롭다면 그의 대안으로 사용할 수 있는 것이 바로 C++의 try/catch 문법입니다. 비주얼 C++에서는 MFC 클래스에 대해서만 적용가능하다는 단점을 갖고 있긴 하지만 이를 이용하면 if 문을 이용해 함수의 리턴 값을 검사할 필요가 없기 때문에 코딩도 간단해지고 코드를 읽기도 좀더 쉬워집니다. “3. 함수의 리턴값 체크”에서 본 코드를 try/catch를 이용하도록 변경해보면 다음과 같습니다.

      // strFilePath가 읽어들이고자 하는 파일의 경로를 가리킨다고 하자.  CFile file;  char strHeader[256];  try  {    file.Open(strFilePath, CFile::modeRead);    file.Read(strHeader, 255);  }  catch(CException e)  {    e.ReportError();    e.Delete();  }

    try로 둘러싸여진 블록 내에서 에러가 발생하면 실행이 바로 catch 블록으로 넘어갑니다. 단 모든 에러가 다 try 블록에 의해 감지되는 것은 아닙니다. 에러가 발생한 경우 그것을 throw 키워드를 이용해 리턴하는 함수들에서만 에러가 감지됩니다. MFC 클래스들은 대부분 에러가 발생하면 CException이란 클래스로부터 계승된 각자의 에러 클래스의 개체를 throw하도록 되어있습니다. CFile 클래스의 경우에는 CFileException이란 클래스가 에러 클래스에 해당하며 CException으로 계승된 클래스입니다. 또 클래스는 아니지만 new로 메모리를 할당하는 경우에도 메모리가 부족하면 예외를 발생시킵니다. 즉 ,new를 이용해서 메모리를 할당하는 경우에는 try/catch를 이용한다면 굳이 메모리 할당이 제대로 되었는지 일일이 검사할 필요가 없다는 이야기입니다.

    사실 try/catch/throw는 그 자체만으로 상당한 지면을 통해 설명해야 하기 때문에 여기서는 이런 것이 있다는 것을 알리는 정도로 끝을 맺겠습니다. 참고로 비주얼 베이직이나 자바, C# 등의 대부분의 현대적인 프로그래밍 언어들은 이런 방식의 에러처리를 지원합니다.


    >

    top

    4> 컴파일러 경고 메시지에 신경쓰자

    대개의 프로그래머들은 컴파일러가 내주는 에러 메시지에만 신경을 쓰고 경고(Warning) 메시지에는 둔감합니다. 그런데 이 경고 메시지를 눈여겨 보면 간혹 모르고 지나쳤을 버그를 잡는 경우가 있습니다. 조사에 의하면 버그를 발견하는데 드는 시간이 90%이고 이를 수정하는데 걸리는 시간은 10%에 불과하다고 합니다. 경고 메시지를 눈여겨 보면 적은 노력으로 버그를 발견할 수 있는 셈입니다. 다양한 경고 메시지 중에서 네 가지를 살펴보도록 하겠습니다.

  • 비초기화 변수 사용 경고

    그 중의 대표적인 것이 바로 변수를 선언은 했지만 그 변수를 초기화하지 않고 사용하는 경우입니다. 예를 들면 다음과 같은 경우가 있습니다.

      {    int nNumber1, nNumber2;    nNumber1 = 100;    nNumber1 += nNumber2;

    nNumber2에는 어떤 값이 들어가 있을지 아무도 모릅니다. nNumber2에는 임의의 값이 들어가 있을 수 있고 이는 nNumber1의 값에도 영향을 주게 됩니다. 위의 코드를 컴파일하면 다음과 같은 컴파일러 경고 메시지가 나타납니다.

    warning C4700: local variable 'nNumber2' used without having been initialized

    컴파일러의 경고 메시지도 유의해서 보는 사람이라면 쉽게 nNumber2가 초기화되지 않고 사용된 사실을 알아차릴 수 있습니다. 사실 이런 종류의 에러는 컴파일러의 경고 메시지가 아니면 찾기가 그리 쉽지 않을 수도 있습니다. 그 이유는 위와 같은 결과가 프로그램의 실행 결과에 큰 영향을 끼치지 않을 수도 있기 때문입니다. 즉 모르고 넘어갈 수도 있다는 것입니다.

  • 함수 내에 값을 리턴하지 않는 플로우 존재

    함수의 코드가 길어지고 그 안의 다양한 부분에서 값을 리턴해야 한다면 실수로 return문을 빼먹을 수가 있습니다. 이 경우에도 앞서 초기화 안 된 변수를 쓸 때와 마찬가지로 그 당시 스택에 있던 아무 값이나 리턴이 되고 또한 마찬가지로 프로그램이 실행되는데 별 지장이 없을 수 있습니다. 그래서 이 역시 좀 찾기 어려운 에러가 될 수 있습니다. 예를 들어 다음과 함수를 보겠습니다.

    int ReturnCheckFunction(int i){  switch(iValue)  {    case 1: return 0;    case 0: return 1;  }}

    위의 함수를 보면 인자 iValue로 1,0이외의 값이 들어오는 경우 return 문이 존재하지 않습니다. 이 경우 스택에 있던 아무 값이나 리턴됩니다. 위의 코드를 컴파일하면 다음과 같은 경고 메시지가 발생합니다.

    warning C4715: 'ReturnCheckFunction' : not all control paths return a value

  • 논리 판단문안에서의 치환문 사용

    가끔들 많이 하는 실수 중의 하나는 if문이나 while 문과 같은 논리 판단문 안에서 == 대신에 실수로 =를 사용하는 것입니다. 이 에러도 경우에 따라서는 아주 찾기 힘든데 컴파일러의 경고 메시지를 눈여겨 보면 아주 쉽게 찾을 수 있습니다. 예를 들어 다음과 같은 코드가 있다고 합시다.

      int iRet;   iRet = CallSomeFunction();   if (iRet = 0)   {     ...   } 

    위의 코드에서 if문을 보면 iRet의 값과 0을 비교한다는 것이 잘못 되어서 iRet에 0을 대입하고 있습니다. 이렇게 되면 이 if 문은 항상 거짓이 됩니다. 이 코드를 컴파일하면 다음과 같은 에러가 발생합니다. WARNING 4706: assignment withing conditional expression 반대의 경우로 =를 써서 변수에 어떤 값을 대입하는 경우에 잘못해서 등호 연산자인 ==를 사용하는 경우도 있습니다.

    i == 0;

    이 것도 컴파일이 됩니다. 단 컴파일러가 다음과 같은 경고 메시지를 내줍니다.

    WARNING 4553: '==' : operator has no effect; did you intend '='?

    항상 컴파일러가 내주는 경고 메시지에 꼭 신경쓰기 바랍니다. 몇십초 동안 잠깐 살펴보는 것으로 여러분이 몇 시간 혹은 며칠동안 고생하는 것을 방지해줄 수 있습니다.

    5> BOOLEAN 타입

    비주얼 C++에는 두 종류의 Boolean 타입이 존재합니다. 하나는 BOOL이고 다른 하나는 bool입니다. 이 두 타입은 모두 다른 타입으로부터 typedef를 이용해 재정의된 것인데 다음과 같이 정의되어 있습니다.

    typedef int BOOL;
    typedef byte bool;

    이 두 가지 중에서 bool 타입을 항상 사용하기를 권합니다. 이 타입의 장점은 다음과 같습니다.

  • 크기가 작습니다. 위에서 볼 수 있듯이 BOOL 타입은 크기가 4바이트이고 bool 타입은 크기가 1바이트입니다. 즉, 커다란 배열을 사용할 일이 있으면 bool이 절대적으로 메모리의 낭비를 줄일 수 있습니다.
  • 안전합니다. BOOL은 사실 TRUE, FALSE이외에도 다른 값이 대입 가능합니다. 예를 들어 다음과 같은 코드를 보고 실행 결과를 예측해보기 바랍니다.
      BOOL bRet = 100;  if (bRet == TRUE)    AfxMessageBox("bRet가 참입니다.“);  else    AfxMessageBox("bRet가 거짓입니다.“);

    BOOL 타입은 원래 TRUE와 FALSE 둘 중의 한 값을 갖도록 되어 있지만 불행히도 위와 같이 bRet에 100을 대입하는 코드는 전혀 문제를 일으키지 않습니다. 그리고 FALSE는 0으로 정의되어 있고 TRUE는 -1로 정의되어 있습니다. 따라서 위의 if문에서 bRet의 값(여기서는 100이 됩니다)과 TRUE는 서로 다른 값이 되어버리기 때문에 두 번째 메시지 박스가 실행됩니다. 즉 BOOL 타입의 문제는 TRUE, FALSE 이외의 다른 값이 들어갈 수 있는 가능성이 있기 때문에 if 문을 어떻게 사용하느냐에 따라서 전혀 다른 실행 결과를 낳는다는 것입니다. 만일 위의 코드에서도 if (bRet)와 같이 사용했다면 if 문이 참이 될 것입니다.

    반면에 bool 타입(혹은 BOOLEAN)은 true와 false라는 단 두 가지의 값만을 가질 수 있습니다. 만일 0이외의 값을 이 타입의 변수에 대입하여도 이 값은 true라는 값으로 바뀌어서 저장됩니다. 다음과 같은 코드를 보기 바랍니다.

      bool bRet = 100;  if (bRet == true)    AfxMessageBox("bRet가 참입니다.“);  else    AfxMessageBox("bRet가 거짓입니다.“);
    위의 if 문은 참이 됩니다. 앞서 설명한 것처럼 bool 타입에서는 0이외의 값은 true로 바뀌어 저장되기 때문입니다. 항상 bool 타입을 사용하기 바랍니다.

    6> 함수 인자 유효성 체크

    이것은 버그를 방지하기 위한 일종의 방법인데 자신이 만드는 함수의 선두 부분에서 주어진 인자가 맞는 범위에 있는지 항상 먼저 체크하는 것입니다. 이 방법은 특히 다른 사람과 사용할 함수를 만들거나 다른 사람이 만든 데이터를 사용하는 함수를 만들 때 아주 효율적인 방법입니다. 즉 자기가 예상하고 있는 데이터들이 들어오는지 사용에 앞서 한번 검사하는 것입니다.

    방금 이야기한 것처럼 이 방법은 공동작업에 아주 쓸모가 많습니다. 사람마다 나름대로의 독특한 이해방식을 갖고 있기 때문에 한참을 이야기해서 함수 인자 및 데이터의 형식에 대해 토론을 하고 결론을 내려도 실제로 구현을 해놓고 보면 서로의 이해가 다른 경우가 많습니다. 이로인한 혼란을 막기 위한 방법 중의 하나가 바로 이 방법입니다.

    예를 들어 어떤 소팅함수가 있는데 소팅 대상이 되는 것이 0부터 2048 사이의 정수라고 합시다. 그러면 그 함수의 선두 부분에 다음과 같은 검사 코드를 넣어두는 것입니다.

      BOOL Sort(int *pnNumbers, int nCount)  {    // 검사 코드를 넣어둡니다.    if (nCount <= 0) // 소팅할 대상이 0보다 작으면 그냥 리턴합니다.      return FALSE;    for(int iIndex = 0;iIndex < nCount;iIndex++)    {      if (pnNumbers[iIndex] < 0||pnNumbers[iIndex] > 2048)      {        printf("%d 번째의 값이 이상합니다. - %d\n", iIndex, pnNumbers[iIndex]);        return FALSE;      }      }    // 실제 소팅 코드가 나온다.    ....  }

    위와 같은 식으로 코딩을 하면 많은 버그를 잡을 수 있습니다. 그런데 위의 코드를 보면서 속도가 느려지지 않을까 하는 걱정을 하는 사람도 있을 것입니다. 그런 걱정이 든다면 위의 코드를 디버그 모드에서 동작하도록 하면 됩니다. 원래 디버그 모드에서 개발을 하고 디버깅이 끝나고 나면 릴리스 모드로 컴파일해서 사용하는 것이 정석이니까요. 그렇게 할 수 없는 경우도 가끔 있습니다. 디버그 모드에서는 잘 동작하는 프로그램이 릴리스 모드에서는 잘 동작하지 않는 경우가 간혹 있습니다. 다시 본론으로 돌아와서 디버그 모드에서 위의 체크 코드를 활성화시키고 싶다면 #ifdef 조건부 컴파일 지시자와 _DEBUG 상수를 이용하면 됩니다. 이 상수는 디버그 모드로 컴파일되는 경우에 비주얼 C++ 컴파일러에 의해 정의가 됩니다.

      bool Sort(int *pnNumbers, int nCount)  {    // 검사 코드를 넣어둡니다.    if (nCount <= 0) // 소팅할 대상이 0보다 작으면 그냥 리턴합니다.      return FALSE;#ifdef _DEBUG     for(int iIndex = 0;iIndex < nCount;iIndex++)    {      if (pnNumbers[iIndex] < 0||pnNumbers[iIndex] > 2048)      {        printf("%d 번째의 값이 이상합니다. - %d\n", iIndex, pnNumbers[iIndex]);        return FALSE;      }      }#endif    // 실제 소팅 코드가 나온다.    ....  }

    #ifdef 다음에 오는 상수가 #define문으로 정의된 것이면 여기서부터 #else 혹은 #endif까지의 코드는 포함되고 결과적으로 컴파일됩니다. 즉 디버그 모드인 경우에만 위의 검사 코드가 포함되는 것입니다. 릴리스 모드에서는 _DEBUG라는 상수가 정의되지 않기 때문에 위의 코드는 포함되지 않습니다. 참고로 #ifndef라는 것도 있습니다. 그 다음에 오는 상수가 정의되어 있지 않으면 참이 되는 것입니다.

    다시 한번 말하지만 디버그 모드는 말그대로 개발시에 디버깅을 위한 컴파일 모드입니다. 이 모드에서는 컴파일러가 코드 최적화를 시도하지 않습니다. 그렇기 때문에 덩치도 크고 속도도 릴리스 모드에 비해 20-40% 가량 늦습니다. 따라서 상품화되거나 외부에 서비스를 하는 프로그램이라면 릴리스 모드로 컴파일되는 것이 필수적입니다. 참고로 비주얼 C++ 닷넷에서는 그림 2처럼 빌드(Build) 메뉴의 구성 관리자(Configuration Manager) 명령을 선택하면 이를 선택할 수 있습니다. 비주얼 C++ 6.0에서는 이 컴파일 모드를 결정하는 것은 Build 메뉴의 Set Active Configuration 명령에서 가능합니다.


    그림 2. 디버그 모드와 릴리스 모드의 선택

    7> 통일된 함수 실행 성공 리턴 값과 시작 인덱스 값

    리턴 값이 있는 함수의 경우 성공을 나타내기 위해 사용하는 값을 통일하기 바랍니다. 예를 들어 어떤 함수에서는 성공을 나타내기 위해 0을 사용하고 다른 함수에서는 1을 사용하고 또 다른 함수에서는 -1을 사용한다고 하면 자신이 만든 함수라 해도 항상 코드를 다시 살펴보아야 하는 문제가 발생하며 이로 인해 버그가 발생할 수도 있습니다. 저는 항상 성공을 나타내는 리턴코드로 0을 사용합니다. 성공을 나타내는 함수의 리턴 값을 통일해서 사용하기 바랍니다.

    시작 인덱스 값도 마찬가지입니다. 예를 들어 열명의 사람에 대한 정보가 있는데 이름이 주어졌을 때 그 사람에 해당하는 번호를 리턴하는 함수를 만든다고 합시다. 자 그럼 이 번호를 0부터 셀 것인지 아니면 1부터 셀 것인지 혼란이 오는 경우가 있습니다. 저는 이런 경우 항상 0부터 세며 꼭 주석이나 문서에 0부터 센다는 것을 명시합니다. C/C++의 배열 인덱스가 0부터 시작하기 때문에 C/C++에서는 무엇인가 숫자 정보의 시작을 0부터 하는 것이 자연스러운 경우가 많습니다. 자신만의 인덱스 시작값 시작 규칙을 만들어 두기 바랍니다. 만일 여러 사람이 같이 일을 한다면 다같이 통일하면 더욱 좋겠지요.


    참고 3. 변수 표기법

    프로그래머의 개인 취향에 따라 변수 이름을 만드는 방법은 참 다양합니다. 아무 생각없이 하는 사람도 많긴 합니다만. 윈도우 운영체제에서는 찰스 시모니(Charles Simony)란 사람이 만든 헝가리안 표기법이란 것을 사용합니다. 이런 이름이 붙은 이유는 이 사람이 헝가리 출신이었기 때문이었고 윈도우에서 이 사람의 표기법을 사용하게 된 것은 이 사람이 마이크로소프트의 초창기 멤버로 많은 일을 했기 때문입니다. 이 표기법은 변수 이름 자체에 그 변수의 타입에 대한 정보를 같이 주는 것이지요. 예를 들면 다음과 같습니다.

    int nCount;         // 정수 변수 앞에는 i나 n을 붙입니다.DWORD dwCount;  // DWORD 타입 앞에는 dw를 붙입니다.bool bReturn;       // Boolean 타입 앞에는 b를 붙입니다.char *lpstrMessage;  // 문자에 대한 포인터앞에는 lpstr 혹은 pstr을 붙입니다.

    여기에다가 윈도우에서는 클래스 멤버 변수의 경우, 그 이름 앞에 m_를 붙이는 것이 일반적입니다. 저 같은 경우는 전역 변수의 경우 그 이름 앞에 g_를 붙입니다. 이렇게 하면 좋은 점은 변수를 보는 순간이 이게 어디 정의되었고 타입이 무엇인지를 알 수 있게 된다는 것입니다. 예를 들어 다음과 같은 코드를 보기 바랍니다.

    m_nCount = nIndex +g_nMinimum;

    위의 코드를 보면 m_nCount는 이 코드가 실행되고 있는 클래스내의 멤버 변수이고 nIndex는 로컬 변수 (흔히 말하는 auto 변수)이고 g_nMinimum은 전역 변수라는 것을 알 수 있습니다. 거기다가 모든 변수가 정수 타입이라는 것도 알 수 있습니다.


    결론

    지금까지 버그를 방지하거나 버그를 잡기 위한 여러 가지 방법들에 대해 알아보았습니다. 사실 제일 중요한 것은 빠르고 간결하며 정확한 코드를 만들겠다는 마음 자세가 아닌가 싶습니다. 이러한 자세가 바탕이 된다면 위의 방법들이 습관으로 만드는 것이 그리 어렵지 않을 것입니다. 사실 방법 하나하나는 대단한 것이 없습니다. 하지만 이것들이 합쳐지면 시너지 효과가 대단합니다. 이것들을 습관으로 만드는 것이 바로 버그를 없애는 프로그래밍의 시작이며 프로그래밍을 처음 배우는 단계에서 이를 습관으로 만들 수 있다면 여러분은 아주 뛰어난 프로그래머가 될 수 있을 것입니다.

    참고문헌

    1. Steve Maguire, Writing Solid Code : Microsoft's Techniques for Developing Bug-Free C Programs, Microsoft Press, 1993
    2. Brian W. Kernighan and Rob Pike, The Practice of Programming, Addison-Wesley, 1999
    3. Steve Maguire, Debugging the Development Process : Practical Strategies for Staying Focused, Hitting Ship Dates, and Building Solid Teams, Microsoft Press, 1994
    4. Steve C McConnell, Code Complete : A Practical Handbook of Software Construction, Microsoft Press, 1993
  • 수정 : TRUE는 -1로... =====> TRUE는 +1로 정의되어 있습니다.

     

    칼루
    나만의 강의 2004. 5. 2. 20:47
    ,

    3.PlayStation 1(one) 2D Format 해부학 - ConverBMP(16Bit)

    ConvertBMP(16Bit)


    작성자 : kallru(신휘재)
    E-Mail : kallru@kornet.net

     

    ※이 문서는 작성자의 허락없이 재배포 할 수 없음을 알려드립니다. 최소한 작성자에 허락 정도는 맡아주시기 바랍니다.

     

    지식은 권한을 보호 받어야 한다.


    - 목  차 -
    10.16Bit의 특징
    11..BYTE와 BIT
    12.16Bit를 24Bit로 만드는 And연산의 힘
    13.포인터의 힘과 주의점

     

    10.16Bit의 특징
    16Bit는 먼저 그 특징과 개념을 이해하기가 좀 힘든데, Bit 개념이 확실히 서지 않은 사람이라면 더욱더 난해할 것이고 Bit 개념이 충분하다면 쉬울 것이다. 나 또한 한참 프로그램에 기초를 배우면서 Bit이니 Byte니 하면서 머리 아플 때 16Bit 강의를 듣게 되었었는데 지금 생각해봐도 그때 선생님이 뭐라고 한지 도저히 이해 할 수 없다. 자 그럼 16Bit의 특징을 살펴 보자.
     16Bit는 말 그대로 16개의 Bit로 1픽셀을 표현한다는 것이다. 1픽셀을 표현하는데 16비트를 사용한다는 것인데, 그래픽을 조금 이라도 아는 사람이라면 RGB와 CMYK를 알 것이다. 컴퓨터는 RGB로 이루어저 있다는 것도 알고 있을 것이다. 그렇다면 1픽셀을 16개의 Bit로 표현한다면 16개의 Bit로 RGB를 표현한다는 소리가 되는 것이다. 그런데 문제는 바로 여기서부터 시작되어진다.
     3으로 16을 나누면 1이 남게 된다. 각각의 RGB 마다 5Bit씩 할당해 준다고 생각하면 1Bit가 남는 것이다. 그럼 이 사태를 어떻게 해결해야 될까? 그래서 나온 것이 바로 2가지 해결책이 나오게 된 것이다.
     첫 번째 방법은 5:5:5 모드라고 해서 각각 RGB를 5:5:5씩 주는 것이다. 그럼 좀 전에 이야기 한 대로 1바이트가 남게 된다. 이 1바이트를 사용하지 않는 것이다. 즉 이건 16Bit가 아니라 15Bit가 되는 것이다. 과거에는 이 방식을 많이 사용했다. 이해를 돕기 위해 간단한 그림을 붙이겠다.


    [X][R][R][R] [R][R][G][G] [G][G][G][B] [B][B][B][B] -> 실제 사용되는 Bit수는 15Bit, 최상위 1Bit는 사용하지 않는다.


     두 번째 방법은 5:6:5 모드라고 하는 것이다. 사람의 눈이 초록색깔에 민감하데나?? 그런 것을 생각해서 G 에 1Bit를 더 주는 방식이다. 그림을 보고 5:5:5 모드와 비교 해본다면 쉽게 이해 될 것이다.


    [R][R][R][R] [R][G][G][G] [G][G][G][B] [B][B][B][B] -> 이것이 실제로 16Bit, 16개의 모든 Bit를 사용한다.

     

    이렇게 16Bit를 가지고 RGB를 표현하게 된다. 그렇다면 각각 5Bit 또는 6Bit를 주는데, 이걸 값으로 보면 R이 가질수 있는 값은 0∼31 까지가 된다. 2진수로 1 1111 이니까 0부터 31까지의 값을 가지게 되는 셈이다. 각 RGB마다 이 정도로 값을 가지면 실제로 거의 모든 색상을 다 표현 할 수 있다고 생각된다. 그래서 아직까지도 사용되어진다.
     물론 약간 모자르다 싶어서 그래픽 부분에선 24Bit를 사용한다. 이 "10"번 파트를 잘 이해 하길 바란다. 어려운 부분은 없지만 Bit 라는게 무엇인지, 그리고 RGB와 Bit, 픽셀들을 잘 알고 있다면 지금, 그리고 앞으로도 문제 없다. 그리고 마지막으로 Tim포맷은 15Bit를 사용한다.

     

    11.BYTE와 BIT
    이번에는 Bit와 Byte의 관계를 설명 하려 한다. 물론 잘 알고 있을 것이지만, 나중에 쉬프트 연산을 하다보면 자꾸 햇갈리는 관계로 여기에 간단하게 기술해두려 한다. 계산을 하다가 햇갈리면 이번 파트를 참고하길 바라며, 또 나도 Byte와 Bit가 하드웨어에서 어떻게 동작하는지, 그런 세세하고 구체적인 것을 모르므로 좀 더 좋은 자료가 있다면 그걸 활용하길 바란다.
     Byte와 Bit관계는 별거 없다. 1Byte는 8Bit라는 것이다. 그럼 16Bit는 몇 Byte일까?? 당연히 2Byte이다. 그렇다면 C/C++에서 2Byte를 할당 받기 위해선 어떤 형으로 선언해야 될까?? short 형이다. 하지만 이런 이미지 계산에서 -‚는 필요 없으므로 unsigned short형을 쓰게 될 것이다. 이것은 WORD형으로 제 정의 되어 있다. 그래서 WORD형을 쓰게 될 것이다. 결국 1픽셀을 표현하기 위해선 2Byte가 필요하게 되는 샘이다.

     

    12.16Bit를 24Bit로 만드는 And연산의 힘
    갑자기 16Bit를 24Bit로 바꾼다고 하니까 이상 할 것이다. 왜 16Bit를 24Bit로 변환해야 하는 것일까?? 그 이유는 BMP는 16Bit모드를 지원하지 않기 때문이다. 뭐 되긴 되겠지만....어쨌든 BMP는 4,8,24Bit를 지원하므로 8 또는 24로 변환해서 써야 될 것이다. 8Bit는 팔레트를 짜야 된다. 그렇게 되면 더욱 복잡해지지만 16Bit를 그대로 살리질 못한다. 더 복잡하지만 제대로 살리지도 못한다면 사용할 필요가 없지 않겠는가? 그래서 24Bit로 변환하게 된 것이다.
     그럼 한번 16Bit와 24Bit를 생각해보자 16Bit는 RGB 각각 5:6:5를 사용한다. 24Bit는 RGB 각각 1Byte(8Bit)를 다 사용한다. 그렇다면 우리는 5->8 로 바꾸어 주면 된다는 결론이 나온다. 5를 8로 늘리려면 3이 더 필요하다 이 3은 어떻게 처리를 할까?? 그건 간단하다 0 값으로 매꾸면 되는 것이다.

     

    [R][R][R][R] [R][G][G][G] [G][G][G][B] [B][B][B][B] -> 이런걸
    [R][R][R][R] [R][0][0][0] <- 이렇게 각각의 RGB를 바꾸어 주면 된다.

    비는 부분 만큼 0 값으로 채워주면 되는 것이다. (당연 0값으로 채워지는 부분은 오른쪽에서부터 3까지가 된다)


    여기서부터 이 글을 읽고 있는 사람은 의문을 가저야 한다. 어떻게 각각의 RGBBit를 컨트롤 해야 하는 것일까??? 여기서 바로 And연산의 진정한 힘!이 나오는 것이다.
     우리는 16Bit에서 RGB가 어떻게 할당되어 있는지 안다. 또 And연산을 알고 있다. 그렇다면 R값을 뽑아내는건 쉽지 않은가?? 우리는 비트마스킹을 해서 R값을, G값을, B값을 가저와야 한다. 아래부터는 예제를 이용해서 이해를 돕겠다.

     

    1101 1001 1100 0011 -> 이런 16Bit가 있다.
    1111 1000 0000 0000 -> 상위 5Bit는 R값이 된다. 상위 5Bit만 1로 &연산 시킨다.
    -----------------------
    1101 1000 0000 0000 -> 최종적으로 뽑혀진 R 값이다.

     

     이렇게 하나씩 뽑아내면 된다. 그런데 여기서 또 문제가 있다.^^;
    바로 1111 1000 0000 0000 이 값은 0∼31 범위를 넘는다는게 문제다. 1Byte 범위는
    0000 0000 이렇게 8Bit 까지만 이기 때문에 이 범위로 맞추어야 한다. 그래서 우리에게 주어지는 것이 바로 쉬프트이다.
     1101 1001 1100 0011 -> 이 녀석중 상위 5Bit인 1101 1 이걸 하위 1Byte로 주어야 한다.
    아님 다 뽑아낸 값을 1Byte로 맞추어 주어도 상관없다. 여기선 먼저 맞춘 후에 비트마스킹 하기로 한다.
     1101 1001 1100 0011 -> 이걸 오른쪽으로 7번 쉬프트 시키면 1Byte 범위 안으로 R 값이 들어오게 된다.
    0000 0000 1101 1000 -> 하튼 이런식으로...물론 앞에 0 으로 가득 차는게 아니라 밀려진 값 만큼 앞으로 돌아온다. 다시 말해
     1100 0011 1101 1000 이런식으로 변하는 것이다. 여기서 우리는
    0000 0000 1111 1111 이렇게 비트 마스킹 하면 0∼31 범위의 R값을 얻을 수가 있다.
    G나 B 값도 이런식으로 밀어서 얻어 오면 된다. 주의 해야 될것이 B 값은 왼쪽으로 옮겨줘야 한다는 것이다.
     1 1000 <- 이런 값을 1100 0000 이렇게 만들어 줘야 하므로 왼쪽으로 밀어주어야 한다는 점과 G는 6Bit를 차지 한다는 점을 잊지 말자.

     

    13.포인터 힘과 주의점
    C언어에는 포인터가 있다. 이 포인터를 이용하면 굉장히 쉽게 여러 가지를 할 수 있는데 포인터가 없었다면 분명 16Bit를 컨트롤 하는데 힘이 들었을 것이다. 포인터를 이용해서 나는 1Byte형으로 되어 있는 이미지데이터를 먼저 2Byte형으로 바꾸어 줄 것이다. 그리고 나서 위에서 설명했던 비트마스킹을 거처서 최종적으로 24Bit로 만든 것을 전강좌에서 24Bit를 설명한 것처럼 할 것이다. 그럼 이제 소스를 보며 이야기 하겠다.

     

     Image16.Size = imgInfo.Width*3*imgInfo.Height;
     Image16.Data = new BYTE[ Image16.Size ];
     ZeroMemory(Image16.Data,Image16.Size);


    가장 먼저 이런 부분을 보고 가야 한다.
    Imaeg16.Size 부분을 보자 Width*3*Height 이렇게 Size를 구하고 있다. 첫 강좌에서 분명 Width*2가 Tim 이미지의 실제 데이터 길이라고 했었다. 그런데 *3을 하고 있다. 그 이유는 이 Image16에 저장될 것은 Tim24Bit가 아니라 BMP24Bit라는 점을 알고 있자. 그러니 당연히 가로*3*세로가 맞다. 어째서 3을 곱해야 된다고 묻는다면 처음부터 다시 보길 바란다.

     

     Temp1 = (WORD *)imgInfo.Data;

     

     다음으로 이런 부분이 나오는데, 위에서 설명했다시피 Byte형을 2Byte형으로 읽는다고 했다. 포인터를 이용해서 위와 같이 Temp1으로 Data를 WORD형 포인터로 지정할수 있다.
    이게 몬 말인가 정리 해보면 imgInfo.Data는 Byte형으로 데이터를 바라보고 있지만 Temp1은 WORD형으로 봐라 이소리이다. 즉 Temp1은 Data를 WORD형으로 취급하게 된다.

     

     BYTE r,g,b;
     for(i=0; i<imgInfo.Size/2; i++)
     {
      b = (Temp1[i] >> 7) & 0xf8;
      g = (Temp1[i] >>2) & 0xf8;
      r = (Temp1[i] <<3) & 0xf8;

      //R,G,B 값 뽑는데 까지 함
      //24 비트로 다시 바꾸어서 넣어주어야 함

      *(Image16.Data + (i*3) + 2) = r;
      *(Image16.Data + (i*3) + 1) = g;
      *(Image16.Data + (i*3) + 0) = b;
     }


    이 부분은 바로 비트마스킹을 하는 부분이다. 바로 위에서 Temp1을 이용해서 WORD형으로 바꾸었다. 그렇다면 XRRRR RGGGGG BBBBB 이렇게 Temp1은 취급하게 되는 것이다. 그럼 여기서 우리는 비트마스킹을 해서 가저오면 된다. 먼저 Temp1을 오른쪽을 7번 밀고 0xf8로 비트마스킹 한다. fc로 비트마스킹 했었는데 fc로 하나 f8로 하나 그게 그거 인 것 같다. g,r도 같은 원리로 비트마스킹 해서 각각에 값을 뽑아낸다.
    뽑아낸 값을 bgr 순으로 24bit처럼 넣어준다. 비트마스킹 할때 이미 0∼31 범위의 값으로 쉬프팅 시킨후 마스킹 시켜서 뽑아 냈으므로 자연적이게 r,g,b 각각 1Byte로 만들어 저있다.
    이걸 다시 Image16.Data에 넣어주면 Image16.Data는 24Bit형으로 만들어지게 되었다.
    이걸 다시 역순정렬 후 출력하거나 저장하거나 하면 되는 것이다. 이건 전강좌에서 이미 다 설명했으니 전강좌를 참고하길 바라며, 이것으로 ConvertBMP(16Bit) 강좌는 마치겠다.

     

    - 후 기
    원래 저번주 주말에 쓰려고 했는데 중간고사와 자격증 시험등으로 인해서 주말에 제대로 쉬지를 못해서 못썼네요. 2틀 뒤에 소풍이여서 또 그때 쓰면 되겠지 하고 생각했는데 갑작스럽게 정보올림피아드 예선에 나가게 되어서 그거 준비하는 것 때문에 소풍도 못가고, 소풍날 학교에서 오후 6시까지.....그렇게 오늘까지 왔네요. 어제 정보올림피아드 예선을 봤구요. 오늘은 옷사러 쇼핑도 갔다가 좀 늦게 집에 들어 왔고....해서 오늘도 못쓸 것 같다..라 생각했는데 오늘 미루면 다음번에 또 미루게 될 것 같아서, 힘들지만 다음을 위해서 쓰게 되었습니다.^^; 음 그 덕에 Tim합체는 조금 미루게 됬군요;;; 어차피 5월 2째주는 노는 날도 많고 하니까...^^; 그럼 다음 강좌는 4,8을 묵어서 하게 될 것 같습니다.

     

    Tim을 분석하고 있는 사람, 하려는 사람, Tim에 대한 실질적인 프로그램 레퍼런스!
    나, 다음에 사람을 위해서 강좌는 계속 됩니다.

    칼루
    나만의 강의 2004. 5. 2. 20:34
    ,

    2.PlayStation 1(one) 2D Format 해부학 - Convert BMP(24Bit)

    4월 18일날 쓴거네요 ^^;

     

    Convert BMP(24Bit)


    작성자 : kallru(신휘재)
    E-mail : kallru@kornet.net


    ※ 이 문서를 다른 곳에 배포할 시에는 최소한 작성자에게 문의 해주시기 바랍니다.
    앞 강좌에 배포는 자유이지만 지금 부터는 100% 제 원 소스이기 때문에 제게 허락을 맡고 배포해주시기 바랍니다. 이것은 제가 군대 갈때까지 효과를 지속합니다.
    ※ 오타, 혹은 잘못 되어진 곳, 잘못 표현되어진 곳등은 연락해주시기 바랍니다.

     

    지식은 권한을 보호받고 공유되어야 한다.

     

    - 목 차 -

    6.비트맵의 종류

    7.DDB의 구조&DIB의 구조

    8.24Bit BMP는 어떻게 생겼나?

    9.이제부터 시작이다!

     

     

    6.비트맵의 종류
    비트맵 파일은 2종류가 있다. 첫 번째는 DDB이고 두 번째는 DIB인데 DDB는 예전 윈도우 3.0 이전 버전에 사용된거라 한다. 이 DDB는 하드웨어적으로 많은 제약이 있었다고 한다. 잘은 모르겠지만 상당히 사용할 때 이런저런 제약들이 있었던 것 같다. 이런 단점을 보안한게 DIB인데 이 DIB는 아주 많은 정보들을 가지고 있다. DDB는 필요한 정보만 가지고 있는데 비해 이 DIB는 어떤 하드웨어에서도 사용할수 있도록 하기 위해서 많은 정보를 가지게 되었다고 한다. 이런이유로 요즘에는 DIB을 사용한다.
     하지만 DDB를 사용하지 않는건 아니다. 사용하지 않았다면 예초에 소개할 필요도 없었을 것이다. 윈도우 API에서 DC로 선택할수 있는건 DDB 뿐이라고 한다. 이유는 DIB에 비해서 속도가 빠르고 복잡하지 않아서 내부적으로 사용하기에는 DIB보다는 DDB가 좋다고 한다. DIB를 DC로 엑세스 할 방법은 아직까지 한번도 못봤다. 그렇다고해서 불가능 하다는 소리는 아니고...아직 나는 배운 것 보다 안배운게 몇십제곱은 더 많으니까..

     

    7.DDB의 구조 & DIB의 구조
    이 강좌가 점차 "윈도우즈 API정복" 책과 같은 목자로 가는 것 같아서 왠지 좀 그렇다. 하지만 이렇게 요점만을 딱 보여주게 제목을 정해야 하므로..^^ 여기서는 자세히 서술하지 않겠다 자세히 보고 싶다면 저 API정복 책을 참고하기 바라며, DDB의 구조를 잠깐 보자면
    typedef struct tagBITMAP {
    LONG  bmType;
    LONG  bmWidth;
    LONG  bmHeight;
    LONG  bmWidthBytes;
    WORD bmplanes;
    WORD bmBitsPixel;
    LPVOID bmBits;
    }BITMAP;
    직접 타이핑 했더니 힘들다. 내가 여기서 다루는건 DIB라서 DDB는 여기까지만 쓰고, 다음은 DIB이다. DIB에 구조체는 복잡하다. 구조체를 보자면

    BITMAPFILEHEADER
    BITMAPINFOHEADER
    RGBQUAD
    비트정보

    이런 순으로 되어 있다. 파일헤더와 인포헤더로 헤더가 2개이고 그 다음 나오는 RGBQUAD는 팔레트이다. 이것까지 지나야 그제서야 이미지 데이터가 나오는 것이다.
    각각의 구조체는 VC++에서 Go To Define...으로 찾아가 보던가 MSDN을 참고 하길 바란다.

     

    8. 24Bit BMP는 어떻게 생겼나?
    이제 비트맵을 좀 알겠다면 24Bit가 어떻게 이루어지는가를 짚고 넘어가야한다.
    24Bit는 한점(픽셀)을 3Byte로 나타내는걸 말한다. 24Bit를 트루컬러라고도 하는데,
    윈도우 바탕화면에서 등록정보를 봐보자 그곳에 해상도 설정과 컬러 설정을 하는 곳을 보면 하이컬러(16) 트루컬러(32)라고 되어 있는걸 볼수가 있다.
     24Bit가 안나온다. 왜 인지는 잘 모르겠지만 주워들은 봐로는 24Bit가 32Bit보다 느리다??
    그런 소리를 들었다. 윈도우가 32Bit 운영체제이기 때문에...잘은 모르겠고 어쨌든 윈도우에서는 24Bit를 외면한건가∼
     본론으로 돌아와서 좀 더 자세하게 24Bit를 알 필요성이 있다. 혹시 이 강좌를 읽는 독자가 RGB가 무엇인지 모른다면 알고 오길 바라며, 24Bit에서는 각각 1Byte씩 사용한다.

    R=1Byte
    G=1Byte
    B=1Byte
    그렇다면 한 점(픽셀)은 3Byte로 표현되는게 맞는 셈이다. 1Byte는 곧 8Bit 이다. 8Bit를 가지고 표현 할수 있는 수는 256, 즉 RGB 각각 0∼255개에 색상을 같는 것이다.
    좀 더 쉽게 말하면 같은 빨강이라 해도 붉고 어두운 빨간색이 있는가 하면 눈이 아플정도로 밝고 화사한 빨간색이 있다. 이렇게 0부터 255단계로 사용할수 있다는 것이다. 이 정도로 단계가 디테일해지다 보니까 사람의 눈으로는 구별해내기가 거의 불가능할 정도로 선명하고 디테일해젔다. 굳이 32Bit 까지 사용할 필요도 없을 것 이다. 3D 분야에서나 사용할 것 같다.
     24Bit에 설명은 여기까지 하고 BMP 이 녀석에 대해서 가장 명심해야 될 것 2가지만 정리하고 다음 강좌로 가자. 혹시 모를 것 같아서 꼭 짚고 넘어가는 것이다.

     

    ①BMP에 이미지 데이터 정보는 거꾸로 되어 있다.
    ②BMP는 4Byte 정렬을 한다.

     

    이 2가지를 꼭 기억하자. 혹시 잘 모르겠다 싶으면 자료를 찾아보길 바란다. 여기서 이걸 다루면 이 목차 만큼 분량이 더 불어나기 때문에...

     

    9.이제부터 시작이다! Tim->Bmp로
    여기서부터는 전 강좌 "TimFormat을 읽자"처럼 전체 소스를 올리지 않고, 부분적으로 중요 포인트를 집고, 거기에 대한 설명을 덧 붙이는 식으로 진행하겠다.
    지금부터 잘 기억하길 바란다. 앞으로 험난하기 때문에 기초를 잘 다저야 한다. 모든 것은 전 단계에서부터 응용되는 수학에 법칙에 따라...-_-; 수학은 기초를 모르면 어렵다. 최근 나는 구구단을 잊어가고 있다.


     소스 강좌에 들어가기 앞서 주석처리를 정하겠다.
    [ // ],[ /* */ ] 이건 기본적으로 C/C++에서 사용되는 주석이다. 이걸로 주석처리가 되어 있는 건 내가 만들면서 처둔 주석이기 때문에 다소 틀린게 존재할지도 모른다. [ || ]이건 강좌를 쓰다가, 이 부분에는 좀 더 주석을 첨부해야 될 것 같을 때 사용할 주석 처리이다.[ :: ]이건 설명부분이다.

     

    그럼 이제부터 소스를 보자

     

    int i,j;
    int BigOffset,BmpOffset,BmpOffset2=0;
    BYTE *Temp,*Temp2;
    Temp = new BYTE[ imgInfo.Size ];
    Temp2 = new BYTE[ imgInfo.Size ];
    ZeroMemory(Temp,imgInfo.Size);
    ZeroMemory(Temp2,imgInfo.Size);
    ::사용할 함수 변수들이다. 이 변수들은 함수가 끝남과 동시에 자동 삭제 된다.
    물론 new는 직접 delete 해줘야 한다.
    ::또 전 강좌에서 설명한 헤더 부분에서 IMAGEDATA Image24 이것을 Image 로 사용하니까 이점을 생각해두길 바란다.
    ::그외 특별히 선언한 변수나 구조체는 없으니까 구조체를 알고 싶으면 "TimFormat을 읽자"를 참고 하길 바란다.

     

    //현재 Offset이 2 인건 24비트 칼라 이기 때문
    //0 1 2 -> 2 1 0 으로 바꾸어야 한다.
    //그러기 위해선 i 값(0,1,2...)에 +2를 한후 Offset 값을 -2 씩 해주면 거꾸로 돌아오는 오프//셋을 만들수가 있다. 다만 이것은 3바이트를 사용하는 24bit에서 사용되는 것이다.
    BigOffset = 2;
    for(i=0; i<imgInfo.Size;i++)
    {
     if(BigOffset==-4) BigOffset=2;
     Temp[i] = imgInfo.Data[i+BigOffset]; //Big 방식으로 바꾸어준다
     BigOffset-=2;
    }

    ::이 로직은 지금도 짜증을 유발시키는, 그러나 아주 중요한 개념인 Little Endian과 Big Endian에 대한 부분이다. 위에 주석에는 Big 방식으로 바꾸어준다고 되어 있지만 아직도 나는 잘 모르겠다 Little를 Big으로 바꾸는 것인지 Big을 Little로 바꾸어주는 것인지..어쨌든 내가 알고 있던건 단 하나! 반대로 바꾸어주어야 한다는 것이다. 난 그거 하나만을 머릿속에 담고 만들었다. -_-
     굉장히 원시적인 방법을 사용했는데 먼저 BigOffset를 2로 초기화 시켜둔다.
    Temp[0]번 일 때 imgInfo.Data[2] 이렇게 되어야 한다. imgInfo.Data[2]번을 Temp[0]번으로 옮겨줘야 한다. 그래야 위에 주석처럼 바꾸어 줄수 있다. 이걸 16진수로 예를 들면 56 45 F2 -> F2 45 56 이렇게 바꾸어 주어야 한다. 표를 통해 좀 더 쉽게 보자

    여기서 보면 중간은 안바뀐다는 사실을 알수가 있다. 좀 더 빠르게 바꾸려 한다면 이 중간 부분을 스킵하도록 로직을 짜면 되겠다. 나는 그렇게 해주지 않았고, 해줄 필요성을 느끼지 못했다.

     

    // *역순 정렬
    //BmpOffset은 파일 데이터에 가장 마지막 부분에서 한칸 -1 한 값을 가지고 있다.
    //가장 마지막 부분에서 한칸 전 부터 마지막까지 읽는다 (이것이 마지막 라인이다.)
    //한바퀴 돌면 다시 -1 하게 된다. 결국 점점 앞으로 올라오며,
    //읽을때에는 + 로 읽기 때문에 한칸 뒤에서부터 읽는 것이다.
    //예) File에 마지막이 200 이고 Width데이터가 20 이라면,
    //180(Offset) 부터 시작해서 200(j를 돌리면서 하나씩 얻는다)까지 읽는다.
    //한 라인을 다 읽었으면 다시 Offset에 -1을 해서 다음 라인으로 간다.
    //160(Offset)부터 180까지 읽는다. 이렇게 0 으로 올라오면서 읽는다.

    for(i=0; i<imgInfo.Height; i++)
    {
     BmpOffset = ((imgInfo.Height-1)-i) * (imgInfo.Width*2);
     for(j=0; j<(imgInfo.Width*2); j++)
     {
      Temp2[BmpOffset2+j] = Temp[BmpOffset+j];  //위에서 Big 방식으로 바       //꾼 값을 bmp에 맞추어서 역                                                          //순으로 정렬한다.
     }
     BmpOffset2 = (imgInfo.Size)-BmpOffset;
    }
    ::Bmp는 이미지 데이터가 거꾸로 되어 있다. 이게 완전히 거꾸로가 아니라 Height만 거꾸로 되어 있다. 그래서 Height를 참고해서 역으로 바꾸는 것인데, BmpOffset에 가장 마지막 줄을 넣는다. 그리고는 I를 돌리면서 하나씩 빼게 된다.
    그럼 결국 BmpOffset은 끝줄부터 첫줄까지 올라오게 된다. j가 하는 일은 BmpOffset이 가리키는 줄을 Temp2로 복사하는 일인데, 여기서 중요한 건 BmpOffset은 아래부터 가르키지만 BmpOffset2는 맨 위에서부터 가르킨다. 즉 BmpOffset은 마지막 줄에서 첫줄로 오지만 BmoOffset2는 가장 첫 줄부터 마지막 줄로 내려간다. 결국 Temp에 가장 마지막 줄을 Temp2로 복사한다. 이런식으로 계속 반복해서 Height를 거꾸로 저장시키게 된다. 여기서 (imgInfo.Width*2) 가 궁금할 법도 한데, 이건 "Timformat을 읽자"에서 Image Width 데이터 길이 계산에서 말했다. 이제 남은건 BMP 헤더를 작성하는 것이다.

    //==================== BMP Info 작성 ========================

    Image.Width = imgInfo.Width/1.5;
    Image.Height = imgInfo.Height;
    Image.Size = imgInfo.Size;

     

    Image.Data = new BYTE[imgInfo.Size];
    ZeroMemory(Image.Data,imgInfo.Size);

    memcpy(Image.Data,Temp2,imgInfo.Size);

     

    // 해더 파일
    BMPFileHeader.bfType   = 19778;
    BMPFileHeader.bfSize   = Image.Size + 54;  <-★
    BMPFileHeader.bfReserved1  = 0;
    BMPFileHeader.bfReserved2  = 0;
    BMPFileHeader.bfOffBits   = 54;  <-★

     

    BMPInfoHeader.biSize   = 40;
    BMPInfoHeader.biWidth   = Image.Width;
    BMPInfoHeader.biHeight   = Image.Height;
    BMPInfoHeader.biPlanes   = 1;
    BMPInfoHeader.biBitCount  = 24;
    BMPInfoHeader.biCompression  = 0;
    BMPInfoHeader.biSizeImage  = 0;
    BMPInfoHeader.biXPelsPerMeter  = 0;
    BMPInfoHeader.biYPelsPerMeter  = 0;
    BMPInfoHeader.biClrUsed  = 0;
    BMPInfoHeader.biClrImportant  = 0;

    delete []Temp;
    delete []Temp2;

    ::여기선 별로 어려운 부분이 없다 BmpFileHeader 하고 BmpInfoHeader을 써주는 것 뿐이다. 다만 여기서 ★인 부분을 봐보자.
    54는 헤더에 크기 인데 왜 ★표까지 하면서 봐야 하는 이유는 나중에 팔레트를 쓰게 되면 이 부분에 팔레트 크기를 + 해주기 때문이다. 잊지 말도록하자. 아, 그리고 마지막 Temp,Temp2를 delete 해주는걸 잊지 말자
    ::여기까지 오면 Image구조체에 기본적으로 필요한 데이터들을 Bmp 방식대로 바꾸어 두었다. Bmp 헤더도 짜놨으니, 메모리 상에 Tim->Bmp로 변환되어 있는 거나 마찬가지이다. 물론 실제로 메모리에는 Tim과 Bmp 모두 올라와 있는 거지만....24Bit는 별로 처리해줄게 없어서 가장 쉽다. RGB가 각각 1Byte씩 차지하는 것도 그렇고.....그럼 이 강좌는 여기서 마치도록 하겠다.

     

    -ConvertBMP(24Bit)를 끝마치며......
    작성하는데 1시간 반 정도된 것 같군요. 24Bit는 상대적으로 가장 쉬운 것 같아요. 물론 제가 팔레트 같은 걸 다루어본 경험이 없어서 그럴지도 모르겠지만...제게는 가장 쉬웠고, 가장 편해서 24Bit 분석부터 시작했었습니다. ^^; 다음은 아마 가장 어려운 16Bit가 기다리고 있습니다. 지금 생각해보면 별로 어려운 것도 아닌데....그럼 이만

     

    Tim을 분석하고 있는 사람, 하려는 사람, Tim에 대한 실질적인 프로그램 레퍼런스!
    나, 다음에 사람을 위해서 강좌는 계속 됩니다.

    칼루
    나만의 강의 2004. 5. 2. 18:33
    ,

    1.PlayStation 1(one) 2D Format 해부학 - TimFormat을 읽자

    Html 안되나..-_-;; 이건 2004년 4월 4일날 쓴거다.

    원래 저번주 주말에 3편을 썼어야 하는데, 자격증 시험이다 모다해서 일요일날 바뻐서 쓸수가 없었다;;


    사실 2틀 뒤에 소풍날이여서 그때 쓰려고 했지만...또 대회다 모다해서 소풍을 못갔다..(그 덕에 졸업사진 촬영을 못함 ㅡ_ㅡ)


    블로그에선 "책갈피" 를 못쓰는군요 ㅜ_ㅜ html이 안되성...


    어쨌든 강좌 본문 시작~~

    ========================= 여 기 서 부 터 ===========================


    Tim Format을 읽자

    작성자 : kallru(신휘재)
    kallru@hanmail.net
    pkallru@gmail.com

    ※이 문서는 아무 곳에나 배포해도 되지만, 출처(작성자)는 반드시 기재해 주시기 바라며,
    이 문서를 아무 곳에 배포해서 생기는 모든 일에 책임은 배포자에게 있음을 알립니다.

     

    - 목  차 -

    1.머리말

    2.TimFormat이란?

    3.용어

    4.TimFormat의 헤더

    5.TimFormat읽기 (C/C++/API)



    1. 머리말
    약 1주일간 tim 파일을 접하면서 이 포맷에 대한 정보가 무척 부족하다고 느꼈다. 해외에는 비교적 PlayStation1에 자국화가 활발하지만, 국내에는 아직 초급단계라 생각되고, 또 모든 정보가 해외로부터 얻고 있는 형편이다. 이번 기회에 tim 포맷을 분석하면서 내가 실수했던 부분부터 실질적인 프로그래밍까지, 내 힘이 닿는 부분까지 상세하게 기술하려 한다. 나 다음에 사람, 그 다음에 사람....앞으로 국내 PlayStation1 자국화에 뛰어들 사람들이 나와 같은 실수를 범하지 않기 위한 참고자료가 되기를 바라며 시작하겠다.
    덧붙여 나는 글제주가 없기 때문에 매끄럽지 않더라도 이해해주길 바란다.


    2. Tim Format 이란?
    나도 이것에 대해 자세히 알지 못한다. 다만 PlayStation1에 2D 그래픽 파일이라는 것뿐..
    PlayStation1에서 사용되는 2D 포맷이므로 자국화에 꼭 필요한 Font는 대부분 이 포맷으로 되어 있다. 압축 여부는 아직 잘 모르겠고, 한가지 확실한 건 2D 포맷이라는 것뿐..^^;


    3.용어
    이 문서에서 사용될 전문용어들이다. 모르는 부분이 나오거나 할 때에는 꼭 한번씩 용어를 확인해보자. 나 또한 아직 헷갈리는 것들이 있다.
    Header(헤더) - 이런 기초적인 것까지 용어 집에 넣으면 너무 분량이 많아지지 않을까 생각되지만, 이 헤더라는 것은 중요해서 넣게 되었다. 헤더라는 것은 보통 어떤 format 이든간에 다 만들어 둔다. 앞으로 나올 파일, 이 포맷에 대한 전체적인 정보를 담고 있다. 그림 포맷으로 예를 들면 Width(가로) Height(세로)는 몇이고, Bit는 몇 비트를 쓰고 있는지, 그 외에 필요한 정보들을 모아서 하나에 구조체를 형성해두는데, 이것이 Header헤더가 되는 것이다.

    CLUT - 컬러 참조 테이블 (Color LookUp Tble, 팔레트에 대한 소니측의 용어)

    Image Org - 이미지의 시작 좌표를 설정하는 것이다. TIM 파일을 플레이스테이션
    의 VRAM에서 사용할 때 쓰는 것입니다. 고치는 작업에서는 특별히 필요하지 않지만
    TIM 이미지와 헤더를 생성하는 프로그램에서는 유용할 것입니다.

    Paltte Org - CLUT 데이터의 시작 좌표를 설정하는 것입니다. 고치는 입장에서는
    특별히 필요하지 않지만 TIM 이미지와 헤더를 생성하는 프로그램에는 유용할 것입
    니다.

    BPP: 픽셀당 비트 수 (Pixels per bit)


    4.Tim Format의 Header(헤더)
    - Header -
    :: 기본적인 헤더, 사이즈는 8바이트이다
    [Signature] 4 Bytes : TIM 형식이라는 것을 알리는 ID Tag
    - 0x10
    - 0x00
    - 0x00
    - 0x00
    [BPP Mode] 1 Byte : 그래픽 모드를 정의한다.
    - 0x08 signifies 4bpp,
    - 0x09 signifies 8bpp,
    - 0x02 signifies 16 bpp,
    - 0x03 signifies 24 bpp mode.
    [Reserved] 3 Bytes : 예약이라는 것인데, bmp 포맷도 그렇고 쓰지 않는다. -_-
    - 0x00
    - 0x00
    - 0x00


    - Palette Info -
    :: 12+ Bytes : 팔레트 정보인데 팔레트 헤더가 12 Byte 이며 CLUTData가 + 되어 진다.
    즉 CLUTData가 200Byte 라면 총 212 Byte가 되는 것이다.

    [Length of the CLUT Data] 4 Bytes : 이것은 CLUT데이터+12 한 전체 크기이다.
    CLUTData공식 : CLUT안에 색깔의 수*CLUTs의 수*2+0xC
    [Palette Org X] 2 Bytes : X coordinate in frame buffer.
    [Palette Org Y] 2 Bytes : Y coordinate in frame buffer.
    [Number of Colors in CLUT] 2 Bytes : Depends on the BPP Mode.
    - The value for 4 bpp is 16, and for 8 bpp is 256.
    [Number of CLUTs] 2 Bytes : CLUT의 개수 총 16개 까지 갖을수 있다.

    [CLUT Data] : Number of CLUTs * Number of Colors in CLUT * 2
    - CLUT의 수 * CLUT안에 색깔의 수 * 2
    - CLUT Data는 최대 16개까지 가능 할 것이다. (추측)
    - 이것이 + 가 되는 대상인 것이다.


     - Image Info -
    :: 12+ Bytes : 이미지 정보, 이미지 헤더(12Byte) + 이미지 데이터

    [Length of the Image Data] 4 Bytes : 이미지 헤더(12Byte) + 이미지 데이터, 총 사이즈
    - 중요!! : Image Width * 2 * Image Height + 0xC 이것이 실제 이미지 데이터 사이즈

    [Image Org X] 2 Bytes : Frame buffer X coordinate.
    [Image Org Y] 2 Bytes : Frame buffer Y coordinate.
    [Image Width] 2 Bytes : 가로 크기이지만 BPP Mode에 따라서 구하는 계산법이 있다.
    [Image Height] 2 Bytes : This is the height of the image in pixels.
    [Image Data] Varies : Contains data on all the pixels in the image.


    ※Image Width 데이터 길이 계산
    4,8BPP에서는 아직 잘 모르겠지만, 24BPP에서는 Width에 * 2를 해줘야 Width의 데이터 길이를 얻을 수 있다. 좀 더 쉽게 말해서 우리가 보통 이미지에 데이터 크기를 구할 때 가장 쉽게, 또 많이 사용하는 것이 Width * Height 이다 이것은 초등학교 시절 사각형에 넓이를 구할 때 배웠을 것이다. 하지만 특이하게도 Tim Format은 이렇게 되어 있지 않다. 실제로 이미지 데이터 크기를 얻기 위해선 Width * 2 * Height로 해줘야 한다. 어떤 이유인지는 모르겠지만 분명 저렇게 해야만 값을 얻을 수가 있었다.
    [+] 16BPP에서는 Width * 3 (RGB 각각 1Byte를 쓰므로 *3)을 해야만 했다. Width * 2 * 3 이라 생각했던 나는 2틀간 삽질을 통해서 Width * 2는 24BPP에서만 그런 듯 하다. 다만 이것은 BMP로 변환할때에 입장이다. TIM 자체 데이터는 어떻게 되어 있는지 모르겠지만 16Bit를 24Bit로 변환할 때 Width * 3으로 해줘야 함을 잊어서는 안되겠다.
     다만, 아직 4, 8BPP에서도 저렇게 구해야 되는지는 모르겠다. 하지만 나는 이것에 대해서 의심이 없다. 아마 4, 8BPP에서도 2를 곱해줘야 될 것 같다. 그럼 이해를 돕기 위해 간단한 예를 하나 설명하고 다음으로 넘어가겠다.
    예) 가로가 100이고 세로가 50인 이미지 파일을 읽어 온다고 가정하자.
    프로그램으로 분명
    for(i=0; i<Height; i++)
       for(j=0; j<Width; j++)
       {
       }
    이런 식으로 for문을 돌리면서 이미지의 데이터를 가저올 것이다. 하지만 Tim에선 이렇게 하면 값을 제대로 얻을 수 없고 다음과 같이 해야 한다.
    for(i=0; i<Height; i++)
       for(j=0; j<Width*2; j++)
       {
       }


    ※Image Width 실제 길이 구하는 계산
    이것은 이미지의 가로 길이를 구하는 것이지, 이미지 가로 데이터 길이가 아니다. 데이터 길이를 구하는 식은 따로 위의 식이다. Tim에선 Width가 모드에 따라서 틀린데, 이것도 모드에 따라서 그것이 틀리다.
    - 4BPP : Image Width * 4
    - 8BPP : Image Width * 2
    - 16BPP : Image Width (아무런 계산할 필요가 없다)
    - 24BPP : Image Width/1.5
    이것은 이미지의 가로길이다. 세로길이는 읽어온 길이가 실제 길이이므로 따로 계산해줄것이 없지만, 가로 길이는 이상하게 되어있다. 이해를 돕기 위해 예를 하나 설명하겠다.

    예) 읽어 오려고 하는 Tim파일이 가로가 100이고 세로가 50이다.
    그럼 위에 Image Info를 읽어 오면 Image Width 와 Image Height에 이미지 길이가 각각 들어오게 된다. 하지만 이것이 BPP 모드에 따라 길이가 다르다.
    실제 길이는 100이지만 현재 BPP가 4BPP라면 분명 Image Width로 넘어온 값은 100이 아니라 100/4인 25로 나올 것이다. 그렇다면 반대로 4BPP 모드에서 실제 Image Width 값을 얻으려면 Image Width * 4를 해서 얻을수 있다. 다시 말해, 가로 길이가 100 짜리 그림은 25라고 헤더에 정의되어 있다. 200짜리 라면 50이라고 되어 있다.


    5.Tim Format 읽기
    5번 파트에 들어가기 전에 이 문서를 읽는 사람은 어느 정도에 프로그래밍 지식이 있어야 하며, C/C++/윈도우API를 알고 있어야 한다. 이러한 전제 하에 진행되기 때문이다. 또 이곳에서 나오는 코드는 내가 1주일간 분석하며 만들었던 TimVeiw의 실제 코드이므로 복사해서 사용해도 무방하다. 다만 좋지 못한 부분과 간결하지 못한 로직이 다수 존재하므로 이해해 주길 바라며, 이 문서를 보는 사람이 적당하게 바꾸어서 사용하기 바란다.

    그럼 이제부터 필요한 구조체를 만들어 보자.
    typedef struct
    {
     //Tim의 전체적인 헤더
     int  Sinnature; //TIM형식 이라는 것을 알리는 ID
     int  BPPMode; //BppMode와 Reserved -> 각각 1바이트,3바이트 합이 4바이트
    }TIMHEADER;
    데이터형에 주의해라. int는 윈도우즈에서 4Byte를 할당받으므로 Tim 헤더의 [Signature] 크기를 만족할 수 있다. 그와 마찬가지로 [BPPMode]를 받는데, BPP는 1Byte 이고 뒤에 예약(Reserved)가 3Byte이다. 두 개를 합치면 이것 역시 4Byte가 되므로 int형으로 선언했다.

    typedef struct
    {
     // - Palette Info -  //
     int CTLength; //Length of the CLUT Data
     short pOrgX;  //Palette Org X
     short pOrgY;  //Palette Org Y
     short ColNum;  //Number of Colors in CLUT
     short Num;  //Number of CLUT
     BYTE *Data;  //CLUT Data : CLUT의 수 * CLUT안에 색깔의 수 * 2
    }PALETTEINFO;
    Data는 BYTE형으로 1Byte씩 가질수 있도록 선언했다.

    typedef struct
    {
     int Size;   //이미지 사이즈 : Width * 2 * Height
     int imgLength;  //Length of the Image Data
     short imgOrgX;  //Image Org X
     short imgOrgY;  //Image Org Y
     short Width;   //Image Width
     short Height;   //Image Height
     BYTE *Data;   //Image Data
    }IMAGEINFO;

    typedef struct
    {
     DWORD Size;
     WORD Width;
     WORD Height;
     BYTE *Data;
    }IMAGEDATA;
    따로 ImageData 구조체를 만들었다. 이것은 내가 필요한 이미지 정보들만 갖는 구조체 이다.

    BITMAPFILEHEADER BMPFileHeader;
    BITMAPINFOHEADER BMPInfoHeader;

    TIMHEADER TIMHeader;
    PALETTEINFO PInfo;
    IMAGEINFO imgInfo;
    IMAGEDATA Image24;

    여기까지 필요한 구조체와 변수이다.
    이제 실제로 읽어보자

    HANDLE Rfpt;   //파일을 읽기 위한 핸들이다.
    DWORD RWSize;

    Rfpt=CreateFile(FileName,
                   GENERIC_READ,0,
                   NULL,OPEN_EXISTING,
                   FILE_ATTRIBUTE_NORMAL,NULL);
    if(Rfpt==INVALID_HANDLE_VALUE)
    {
     ::MessageBox(::GetActiveWindow(),FileName,"Error",MB_OK);
     return;
    }

    //가장 먼저 Sinnature를 읽어서 이 파일이 Tim 파일인지 확인한다.
    나는 확인하는 부분을 생략했다.
    ReadFile(Rfpt,&TIMHeader.Sinnature,sizeof(int),&RWSize,NULL);
    ReadFile(Rfpt,&TIMHeader.BPPMode,sizeof(int),&RWSize,NULL);

    //아래 코드는 BPPMode가 0x02(16BPP)와 0x03(24BPP)가 아니라면, 즉 4, 8 이라면
    //팔레트 정보를 읽어야 하므로 그 부분에 대한 코드이다.
    //4BPP,8BPP는 CLUT가 있다.
    if(TIMHeader.BPPMode != 0x02 && //4BPP,8BPP ↓ in Src
     TIMHeader.BPPMode != 0x03) //16BPP,24BPP라면 건너뛴다.
    {
     //---- Palette Info Read ------//
     ReadFile(Rfpt,&PInfo.CTLength,4,&RWSize,NULL);
     ReadFile(Rfpt,&PInfo.pOrgX,2,&RWSize,NULL);
     ReadFile(Rfpt,&PInfo.pOrgY,2,&RWSize,NULL);
     ReadFile(Rfpt,&PInfo.ColNum,2,&RWSize,NULL);
     ReadFile(Rfpt,&PInfo.Num,2,&RWSize,NULL);
     
     PInfo.Data = new BYTE[PInfo.Num*PInfo.ColNum*2]; // Size : CLUT수 * CLUT컬러수 * 2  
     ReadFile(Rfpt,PInfo.Data,PInfo.Num*PInfo.ColNum*2,
      &RWSize,NULL); //CLUT Data Read
    }
    //16BPP,24BPP는 CLUT가 없다 ImageINFO만 가지고 있다.
    //------ Image Read ------//
    ReadFile(Rfpt,&imgInfo.imgLength,4,&RWSize,NULL);
    ReadFile(Rfpt,&imgInfo.imgOrgX,2,&RWSize,NULL);
    ReadFile(Rfpt,&imgInfo.imgOrgY,2,&RWSize,NULL);
    ReadFile(Rfpt,&imgInfo.Width,2,&RWSize,NULL);
    ReadFile(Rfpt,&imgInfo.Height,2,&RWSize,NULL);

    //위에서 언급했다시피 실제 Width 데이터 길이를 얻기 위해선 * 2를 해야 한다.
    //다시 강조하지만 데이터 길이와, 이미지 길이는 다른것이며, 이미지 길이는 각 BPPMod
     //마다 계산해주는 값이 틀리다.
    imgInfo.Size = imgInfo.Width*2*imgInfo.Height;

    imgInfo.Data = new BYTE[ imgInfo.Size ];
    ZeroMemory(imgInfo.Data, imgInfo.Size);

    ReadFile(Rfpt,imgInfo.Data,imgInfo.Size,&RWSize,NULL);
    CloseHandle(Rfpt);
    }


    - 강좌는 여기까지
    주석도 적절하게 붙여놨고, 이 문서를 작성하면서 덧붙여 쓴 주석도 있으니 이 소스를 보는데 에는 큰 무리가 없으리라 봅니다 [TimFormat을 읽자]는 여기서 마치도록 하겠습니다.
    이것은 1편에 불과합니다. 아직 16비트를 다 하지 못해서.....앞으로 화면 출력에 대해서 잠깐 언급하자면, 제가 선택한 것은 바로 bmp 변환후 출력입니다. 4일 정도 동안 Tim 데이터를 바로 화면에 뿌리기 위해서 Directdraw도 써보았지만 제가 아직 실력이 안 되는 거라 그런지 해답을 찾지 못했습니다. 그래서 bmp로 변환 후에 bmp를 화면에 뿌리던가 하는 방법이 좋을 것 같아서 이 방법으로 나가겠습니다.
     다음부터는 ConvertBMP 강좌를 쓰겠습니다. 아마 이 강좌는 4개, 또는 3개 정도로 나뉘어질 것 같네요. 24BPP, 16BPP, 8BPP, 4BPP로 나누던가 24,16,(8,4) 이렇게 나누던가...어쨌든 이게 시작이니까..그럼 16BPP나 계속 풀어야겠네요.

    칼루
    나만의 강의 2004. 5. 1. 19:30
    ,

    여기는 이제!!

    내가 가끔 쓰게 되는 강의들을 여기에도 올려야지!

    게임 개발 및 기타 장르를 가리지 않고, 내 스타일대로 쓰는 내 강좌들을 이곳에도!!

    칼루
    나만의 강의 2004. 4. 28. 20:23
    ,
    Powerd by Tistory, designed by criuce
    rss