결과물:
선이수 과목으로 CG개를 요구합니다.
일반적으로 텍스쳐 매핑은 메쉬에 미리 지정된 texture coordinate를 사용하여 진행됩니다.
여기서 texture coordinate 대신에 다른 coordinate을 넣어서 매핑하면 다른 결과물이 나올 겁니다.
목표는 오브젝트의 평행 변환에는 영향을 받지 않고 회전변환에만 영향을 받는 texture mapping입니다.
이를 위해 (World coordinate에서의 position) && (local coordinate rotation) && (No scaling)을 만들고자 했습니다. 이와 동시에, 다음 조건들을 생각해야 합니다.
- 우리가 실제로 사용할 죄표는 object의 local coordinate 기준에서의 좌표이므로 사실 위에서 이야기한 것을 local coord로 translate하여 적용해야 합니다.
- shader 외부에서 변수를 받지 않고 하려면 쉐이더에서 지원하는 변수들로 구성해야 합니다. shader 외부에서 변수를 받아올 순 있지만 코드 짜기도 귀찮고 성능 저하도 있을 수 있습니다.
- 다행히 유니티 shader에서는 unity_ObjectToWorld와 unity_WorldToObject 두 개의 4x4 행렬을 지원하여 이를 통해 world coord와 object coord를 왔다갔다할 수 있습니다(ObjectToWorld가 model matrix와 동치입니다).
- 그 외에 쓸 만한 것은 local coordinate 하에서의 vertex position 정도입니다.
그러면 이제 생각해 봅시다.
먼저 좌표변환 matrix에 대해 생각해 봅시다. 여기서의 affine matrix는 (translation mat) * (rotation mat) * (Scale mat) 가 (이 순서대로) 곱해진 형태라고 생각합니다. 각자의 성질은 CG개에서 잘 배웠다고 간주합니다.
그럼 이제 (World coordinate에서의 position)을 (World coord 중심에서 Object coord 중심까지의 vector) + (local position)으로 생각해 봅시다. 그러면 전항은 Scaling이 안 걸려 있으니 rotation만 바꿔주면 되고, 후항은 이미 rotation항이 올바르니 scaling만 처리하면 됩니다. 선형변환이니까 이렇게 분해해서 처리해도 됩니다.
그럼 이제 코드를 봐 봅시다.
float4x4 mat = transpose(unity_ObjectToWorld);
float3 scale = float3(length(mat[0]), length(mat[1]), length(mat[2]));
float3 coord = IN.localCoord + mul(unity_WorldToObject, float4(mat[3].xyz, 0)).xyz;
coord *= scale;
먼저 scale을 구하는 것부터 시작합시다. 전 문단에서 local position의 scale은 local coord 기준이므로 이를 world coord로 바꾸려면 objectToWorld의 scale factor를 곱해줘야 합니다. 아쉽게도, 우리는 이를 결과물로부터 역산해야 합니다! translation matrix를 없애는 건 쉽게 생각할 수 있지만, rotation * scale을 분해하는 건 쉽지 않습니다.
여기서 rotation matrix와 scale matrix가 어떤 꼴인지 생각해 봅시다.
각각 rotation과 scale입니다. 이 둘이 (rotation) * (scale)으로 곱해져 있습니다. 이 결과물을 RS이라고 해 봅시다. 이 둘을 곱해 보면 놀랍게도 다음을 알 수 있습니다.
- v_x = sqrt([RS_1_1]^2 + [RS_2_1]^2 + [RS_3_1]^2)
- v_y = sqrt([RS_1_2]^2 + [RS_2_2]^2 + [RS_3_2]^2)
- v_z = sqrt([RS_1_3]^2 + [RS_2_3]^2 + [RS_3_3]^2)
이걸 4x4 affine matrix에서 생각하면, affine matrix 아랫줄이 0 0 0 1이니까 확장을 해도 값이 변하지 않고 그러므로 - v_x = affine matrix의 첫 번째 열백터의 길이
- v_y = affine matrix의 두 번째 열백터의 길이
- v_z = affine matrix의 세 번째 열백터의 길이
라고 생각할 수 있습니다. 그리고, 이후에 필요한 (World coord 중심에서 Object coord 중심까지의 vector)가 objectToWorld matrix의 마지막 열벡터의 xyz입니다. 쉐이더에서는 행렬의 행을 벡터 취급할 수 있어서, 저는 ObjectToWorld matrix를 transpose해서 사용했습니다. 그래서 우리가 구하고자 하는 후항이 IN.localCoord * scale 로 표현되었습니다.
전항은 아까 말했던 objectToWorld matrix의 마지막 열벡터의 xyz에서 출발합니다. rotation을 object로 맞춰야 하니 WorldToObject matrix를 곱해 줍시다. translation이 끼어들어오는 건 싫으니 point를 vector로 바꿔주는 걸 잊지 맙시다. 마지막으로 WorldToObject를 곱하면서 scale이 들어갔으니 다시 보정해야 합니다. 그래서 전항은 mul(unity_WorldToObject, float4(mat[3].xyz, 0)).xyz * scale이 되었습니다. 두 항 모두 scale을 곱하니 뒤로 빼 줍시다. 그 결과 위에서 보았던 코드가 완성되었습니다.
이렇게 Object rotation에만 영향을 받는 좌표계를 만들어 texture coord로 사용할 수 있게 되었습니다. 긴 글 읽어 주셔서 감사합니다.