셰이더에 행렬을 전달하기 전에 다음과 같이 보통 Transpose를 취해야 한다.
그 이유를 검색해 보면 DX는 행렬을 row-major로 다루고, HLSL에선 column-major로 다루기에 전치로 전달하여 맞춰줘야 하기 때문이라고 한다. 맞는 말이겠지만 실제로 디버깅을 해보고 셰이더에서 행렬을 계산하는 코드를 보면 더욱 수긍이 간다.
테스트를 위해 셰이더에 다음과 같은 행렬을 전치하여 전달하였다.
이 행렬과 전치를 취한 행렬은 다음과 같다.
셰이더에서 위의 전치 행렬을 World로 받고 벡터 input.Pos와 곱연산을 하는 코드는 다음과 같다. 아래 연산 결과의 첫 요소는 실제로 input.Pos(1,1,-1,1)과 전치 취하기 전 행렬의 첫 열벡터인 (1,0,0,4)의 곱이여야 한다. (실제로 전치 행렬의 첫 열벡터는 1 0 0 0 이지만 전치를 취해야 정상적인 결과가 나온다 했으므로, 실제론 기존 행렬의 열벡터인 1 0 0 4와 곱해지는 결과가 나와야 함) 아래에서 실제로 전치 행렬을 어떻게 활용해야 기존 행렬과의 곱 효과를 나타내는지 확인해보자.
// input.Pos: (1, 1, -1, 1)
output.Pos = mul( input.Pos, World );
이를 어떻게 계산하는지 확인해 보면 다음과 같다.
dp4 명령어는 4개의 요소를 가지는 데이터 간의 dot product(내적) 명령어이다. v0과 cb0을 내적하여 r0에 저장하는 명령어다. 행렬 곱셈에 웬 내적? 이라 생각한다면 행렬의 곱셈을 다시 생각해보면 이해가 간다.
A(1, 1, 1, 1)과 B(2, 2, 2, 2)의 각 요소를 곱하여 더하는 것이 내적이므로 행렬의 곱셈에서 행과 열을 곱하고 더하는 것과 동일하기 때문에 내적 명령어로 처리한다.
한편, cb0은 constant buffer를 의미하는데 여기에 world 행렬을 전달했으니 world 행렬을 의미한다. cb[i]는 i번 째 행을 의미한다. 따라서 위 코드는 행 벡터를 world 행렬의 각 행과 내적하는 것이다. 열이 아닌 행과의 내적이다. 다행히 우리는 셰이더로 보내기 전 전치를 취했으므로 첫 연산은 (1 0 0 4)와 내적하게 된다.
즉 (1,1,-1,1)과 (1,0,0,4)의 내적이다. 이는 아까 기대했던 연산과 동일하다. 왜 이런 식으로 계산하도록 설계되었는지 생각해보려면 메모리의 locality 성질에 대해 알아야 한다.
간단하게 설명하면 어떤 메모리를 참조하면 다음에 그 메모리와 인접한 메모리를 사용할 확률이 높다고 가정하고 인접 메모리 값들을 cache 메모리에 등록해 놓는 것이다. 참조한 메모리가 cache에 존재하는 것을 hit이라 하는데, 배열과 같이 연속된 메모리를 사용할 경우 hit될 확률이 높아진다.
행렬은 각 행을 연속된 메모리로 저장하지만, 열은 연속된 메모리가 아닐 수 있다. 따라서 매번 불연속 메모리를 참조하게 될 수 있다. 즉 cache hit 확률이 떨어진다. 때문에 전치를 취하여 곱해질 열을 행에 배치하고 내적을 통해 연속된 메모리를 참조할 수 있도록 하여 효율을 높인 것이라 볼 수 있다.
그런데 찝찝한 부분이 하나 있다. hlsl에서 행렬은 column-major라고 했는데, 그럼 행이 아니라 열을 연속된 메모리로 저장한다고 알고 있는데 그렇게 되면 위에서 설명한 내용들과 좀 맞지 않는 것 같다. 누가 이 부분좀 짚어주면 좋겠다~
'게임 공부 > DirectX' 카테고리의 다른 글
쿼터니언 사용 가이드 (0) | 2021.08.18 |
---|---|
Geometry Shader (0) | 2021.05.27 |
XMMatrixLookAtLH과 XMMatrixLookToLH 차이 (0) | 2021.05.04 |
[DX11] UpdateSubresource 와 map은 각각 언제? (0) | 2021.04.18 |
Blend state (0) | 2021.04.03 |
댓글