지난 번에 Draw를 하기 위한 절차를 알아 보았다. 이번엔 직접 화면의 픽셀을 변경해 볼 것이다. 다시 한번 Draw 과정을 살펴보면 적절한 초기화 후 보조 표면에 Lock을 걸고, 얻은 보조 표면의 메모리 주소를 통해 write을 한다. 그리고 Unlock 후 blt을 통해 보조 표면에 작업한 것을 주 표면에 전송하여 스크린에 보이도록 한다.
우선 Lock을 통해 다음과 같이 보조 표면의 시작 주소와 pitch를 얻었다.
// 예외 처리는 생략한다.
DDSURFACEDESC2 ddsc = {};
ddsc.dwSize = sizeof(DDSURFACEDESC2);
m_pDDBack->Lock(nullptr, &ddsc, DDLOCK_WAIT, nullptr);
// 보조 표면의 시작 주소와 pitch
m_pLockedBackBuffer = static_cast<char*>(ddsc.lpSurface);
m_dwLockedBackBufferPitch = ddsc.lPitch;
lpSurface는 void* 타입이며 char*으로 변환하여 따로 저장해둔다. pitch 또한 따로 저장해둔다. 위 코드의 경우 클래스 내에 이미 멤버 변수로 존재한다고 가정한 것이다. lPitch는 DWORD(4bytes) 타입이다.
이제 보조 표면에 위 정보를 이용하여 픽셀을 설정할 것이다. 여기서 픽셀을 설정한다는 것은 보조 표면의 주소가 가리키는 곳의 메모리를 수정한다는 의미이다. 우선 RED 값을 50개 정도 연속으로 찍어볼 건데 그전에 색상 값에 대해 잠깐 살펴볼 필요가 있다. 보통 RGB 값은 RGB 매크로를 이용하여 얻을 수 있다. 이 값은 COLORREF 타입이며 DWORD와 동일한 타입이다. 해당 타입은 우리의 예상과는 다르게 R,G,B를 0x00bbggrr 형식으로 저장한다. (상위 2바이트는 00으로 고정) 즉 RGB(255, 0, 0)을 통해 RED 값을 생성하면 0x00FF0000이 아닌 0x000000FF 형태가 된다. 이를 GDI의 SetPixel에 전달하면 의도대로 RED로 설정이 된다.
하지만 이 값을 표면에 직접 write하게 되면 RED가 아닌 BLUE가 된다. 즉 버퍼에 직접 write할 땐 우리가 흔히 알고 있는 0x00rrggbb 형식으로 설정해 주어야 한다. 다음은 RED 값 설정을 위해 버퍼에 직접 0x00FF0000을 write한 것이다.
Intel의 x86, x64와 같은 H/W 환경인 경우 little endian으로 저장되기 때문에 0x00FF0000와 같은 값을 저장하면 위와 같이 거꾸로 저장된다. 상위 2번째 바이트에 FF를 설정했지만 위 그림에선 낮은 주소에 FF가 설정된 것을 볼 수 있다. SetPixel 함수에 RGB(255, 0, 0), 즉 0x000000FF를 저장해도 결국은 위와 같이 저장된다. 이는 아마 SetPixel 내부에서 변경되어 저장되지 않을까 싶다. 어쨌든 컬러값을 의도대로 설정하려면 위 그림의 메모리 형태로 저장되어야 한다는 것만 기억하면 된다.
여기선 RGB 매크로를 사용하지 않을 것이다. 다음은 시작 주소로부터 연속으로 200바이트 만큼을 RED 값으로 설정한다.
const DWORD dwSrc = 0x00FF0000; // RED
char* pDest = m_pLockedBackBuffer;
// 시작 주소부터 4바이트 단위로 dwSrc 값을 저장한다.
for (size_t x = 0; x < 50; ++x)
{
*(DWORD*)pDest = dwSrc;
pDest += 4;
// 위 두줄 대신 다음과 같이도 가능하다.
// ((DWORD*)pDest)[x] = dwSrc;
}
색상 값으로 사용한 DWORD는 4바이트기 때문에 pDest에도 4바이트 단위로 write해야 한다. 그러나 pDest는 char* 타입이기 때문에 DWORD* 타입으로 형변환 후 값을 write한다. 그리고 4바이트 점프해서 다음 픽셀에 또 write을 반복한다. 총 4바이트씩 연속으로 50번 반복하므로 빨간줄이 그어져 있을 것이다. 다음은 위 코드를 실행한 결과이다.
잘 보이진 않는데, 어쨌든 빨간줄이 미세하게 보이긴 한다.
이번엔 write할 시작 주소를 변경해서 잘 보이게 그려보자. 이제부터 pitch를 활용해야 한다. 아래 그림을 보면 쉽게 이해가 될 것이라 생각한다. 아래와 같은 연산이 가능한 것은 char*은 연속적인 메모리이기 때문이다. 쉬운 이해를 위해 행을 나누어 생각하지만 실제론 행의 끝과 다음 행의 시작이 이어지는 연속되는 메모리 값이다.
+ 내용 추가) 항상 행의 끝과 다음 행의 시작이 이어지는 내용은 아닐 수 있다. 무슨 말이냐면 한 행의 마지막과 다음 행의 시작 사이에 어떤 추가적인 메모리가 끼어 있을 수 있다. 추가적인 메모리가 없을 경우 pitch는 width * 픽셀당 바이트 수가 되지만 추가적인 메모리가 끼어 있다면 width * 픽셀당 바이트 수 + α 가 된다. 어쨌든 Lock을 통해 얻은 pitch를 사용하면 문제가 없다는 것만 기억하자.
시작 주소를 다음과 같이 변경하면 다음과 같은 결과가 나온다.
char* pDest = m_pLockedBackBuffer + (m_dwLockedBackBufferPitch * 50);
이제 좀 잘 보인다.
다음은 더 명확히 보이기 위해 사각형을 그려볼 것이다. 사각형을 그리기 위해선 위 작업을 행 단위로 반복하면 된다. 워낙 간단해서 쉽게 이해할 것이다.
char* pDest = m_pLockedBackBuffer + (m_dwLockedBackBufferPitch * 50);
char* pBegin = pDest;
for (size_t y = 0; y < 50; ++y)
{
for (size_t x = 0; x < 50; ++x)
{
*(DWORD*)pDest = dwSrc;
pDest += 4;
}
// 이전의 시작 주소에서 pitch를 더해
// 아래 행의 시작 주소로 이동한다.
pDest = pBegin + m_dwLockedBackBufferPitch;
pBegin = pDest;
}
위 소스를 출력하면 다음과 같은 사각형이 그려진다.
다음 포스팅에선 키보드 방향키를 조작해 사각형을 이동시키는 예제를 살펴본다.
'게임 공부 > DirectX' 카테고리의 다른 글
[DirectX] Direct3D 11 programming (1) (0) | 2021.03.05 |
---|---|
DirectDraw 학습 (4) - 사각형 이동시켜보기 (1) | 2020.11.23 |
DirectDraw 학습 (2) - Draw (0) | 2020.11.17 |
DirectDraw 학습 (1) - Initialization (0) | 2020.11.15 |
행렬 변환에 대해 알아보자 (0) | 2020.07.05 |
댓글