하늘 까는 걸 한다.
방법이 여러 가지 있다.
먼저 생각할 수 있는 방법은 큐브모양 형태로 만들어 준 다음에 카메라를 따라다니게 붙여주면 카메라가 움직이더라도 큐브가 같이 움직이니까 계속 하늘이 보이게 될 것이다. 하지만 이렇게 하면 사전작업이 필요해서 좀 더 간단하게 만들 수 있는 스피어를 이용한 방법으로 만들어 줄 것이다.
큐브가 아니라 스피어로 만들었을 때 문제가 몇 가지가 있다.
1. 캐릭터가 회전할 때 스피어도 같이 회전하면 안되는 걸 신경 써야 한다.
2. 물체가 있을 때 구형을 하나 만들어서 플레이어 위치에 구를 배치하면 구가 뒷면이라 보이지 않는다. 이걸 뒤집어 줘야 한다. 쉐이더에 그 기능이 있다.
3. 하늘 뒤에 물체가 있으면 물체가 안그려지는 걸 신경 써야 한다.
이번 시간에는 2번 3번에 대해 구현하는 실습을 해 본다.
1. 스피어의 안 쪽 면이 보이게 하기
2. 구의 vertex가 카메라 촬영 범위의 끝에 붙어야 하는데 그걸 할 수 있는 방법 중 하나는 쉐이더 쪽에서 하늘을 그릴 때 z값을 1에 가깝게 설정하는 것이다. 마치 카메라의 범위 끝에 붙어있는 마냥 조절을 해 주는 것이다.
이 둘을 쉐이더 조작을 통해서 만들어 보는 실습을 해본다.
1. SkyDemo 클래스와 18. SkyDemo.fx를 만들고 세팅하기
탐색기에서 TweenDemo 클래스를 복분 하고, SkyDemo라고 한 뒤 AssimpTool/Game 필터에 넣는다. 코드를 SkyDemo에 맞게 수정한다. CreateKachujin은 삭제한다. 지금 작업하고 있는 방식에서는 쉐이더가 섞이면 안 되기 때문이다.
SkyDemo::Init에서
_shader = make_shared<Shader>(L"18. SkyDemo.fx");
이렇게 수정한다.
Main.cpp에서도
#include "SkyDemo.h
desc.app = make_shared<SkyDemo>();
이렇게 한다.
탐색기에서 17. TweenDemo.fx를 복붙해서 18. SkyDemo.fx로 하고
Client/Shader/Week3에 넣는다.
이 상태에서 Client를 빌드한다.
엔진부는 안건드려도 된다.
2. 18. SkyDemo.fx에서 스피어의 뒷면이 보이게 하고, 구의 vertex가 카메라 촬영 범위 끝에 위치하게 하기 위해 VS에서 강제로 좌표를 조작하는 작업하기
쉐이더 18. SkyDemo.fx를 기반으로 작업을 해본다.
Sky와 관련 없는 코드들은 삭제한다.
1) 스피어의 뒷면이 보이게 하기
그런 부분들을 00. Global.fx에서 관리하고 있다.
RasterizerState에서 설정하는 부분이다.
RasterizerState FrontCounterClockwiseTrue
{
FrontCounterClockwise = true;
};
시계 방향이 정면이냐 후면이냐 관리하는 옵션이다.
2) 코드 정리하기
18. SkyDemo.fx로 다시 가서
초심으로 돌아가 VS의 return 값을 VS_OUT으로 하고 VS_OUT을 정의한다.
struct VS_OUT
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD;
};
VS의 기존의 코드들을 삭제한다.
VS_OUT VS(VertexTextureNormalTangent input)
VS의 매개변수는 Blend가 빠진 VertexTextureNormalTangent로 한다.
3) input.position이 perspective 좌표계에서 깊이의 값이 1에 가까운 수가 나오게 z에 w*0.999999를 넣기
월드 변환한 다음에 뷰 행렬을 곱하는 거 자체가 결국에는 카메라 기준으로 하는 좌표를 만들어 주는 거였는데 그럼 이 아이가 일단은 뷰 스페이스 상에서는 좌표가 어떻게 될까?
뷰 스페이스 상에서 원점이 되는 것이다.
float4 worldPos = mul(input.position, W);
그렇기 때문에 이 world를 곱하는 연산을 굳이 하지 않고 건너뛴다.
매개 변수로 받은 input.position이 원점으로 왔다고 가정을 하면,
그러면 View로 넘어갈 때
float4 viewPos = mul(input.position, V);
이렇게 매개변수로 받은 input.position에 V행렬을 바로 곱하는 것을 고려할 수 있다.
이렇게 World행렬을 건너뛰고 View포지션으로 바로 스킵하는 꼼수를 이용할 수 있다.
World 좌표는 일단 0, 0, 0으로 고정을 했다.
여기다가 View 행렬을 곱해서 View 스페이스의 원점이 되길 원한다.
그럼에도 불구하고 회전된 값은 유지하고 싶다.
바라보는 방향에 따라서 다른 하늘이 보여야 하기 때문에 정확하게 회전값만 먹이고 싶을 때
View에서 좌표는 인정하지 않고 회전과 관련된 부분만 적용하고 싶다고 했을 때 사용할 수 있는 방법이 무엇이었을까?
float4 viewPos = mul(float4(input.position.xyz, 0), V);//xyzw중 w를 0으로 한뒤 V를 곱하는 거
이렇게 translation은 영향을 안 받고 그냥 카메라의 회전에 따라서 물체의 회전만 받아주는 게 된다.
float4 clipSpacePos = mul(viewPos, P);
clipSpacePos는 어떤 특징을 가지고 있었나?
최종적인 목적은
// xy[-1~1] z[0~1]
이 범위에 일단은 투영을 하는 게 목표였다. 2D로 가기 위한 중간 단계였다.
하지만 P를 곱했을 때 이 범위의 좌표가 되었을까?
엄밀히 말하면 Projection 행렬을 구했을 때 하나의 행렬식으로 X, Y의 값을 표현하는 게 불가능했기 때문에 z값을 w에 넣어가지고 살리고 그런 게 있었다.(https://devriripong.tistory.com/119) 나중에 rasterizer 단계에서 넘어가면 걔가 알아서 z 나누기까지 해가지고 사실상 PS로 넘어가는 거였다.
이렇게 한 단계가 더 있었다. clipSpacePos까지는 아직 z를 나누기 이전 상태다라고 볼 수가 있다.
반대로 말하면 여기서 좌표를 조작을 해버리면
output.position = clipPos.xyzw;
output.position.z = output.position.w;
이러면 넘어갔을 때 물체의 정점의 깊이값은 뭐가 될까?
z값은 뭐가 될까?
// Rasterizer -> w에다가 z값을 보존해서
// x/w y/w z/w 이런 식으로 나눠주는 역할을 한다.
// x/w y/w z/w 1 이렇게 된다.
근데 우리가 지금 한 거는
왠지는 모르겠지만 z를 w로 강제로 세팅을 해놨다.
그렇다는 것은 rasterizer에 가서 PS단계로 넘어갈 때는 어차피 w를 z에 넣어 놨기 때문에 뭐가 됐건 상관없이 최종적으로 깊이는 무조건 1이 뜬다.
사실상 맨 끝의 깊이에 있는 것처럼 계산이 된다는 것이다.
z에 0~1 사이의 비율을 맞추기 위한 무엇인가가 들어가야 하는데 그게 아니라 무조건 w값으로 밀어줘서 나중에 w로 모든 z나누기가 이뤄진다고 했을 때 그때 어차피 z가 1로 뜨게끔 강제 조작을 한 상태라고 볼 수 있는 것이다.
output.position.z = output.position.w * 0.999999f;
아니면 이렇게 거의 1에 가까운 수치를 넣어가지고 거의 끝까지 가게끔 만들어 준다.
1로 했으면 비교할 때 less, less equal 같은 조건들이 있는데 같을 때도 인정이 되게끔 판별을 하게 만들어 줘야 한다. 1에 가까운 무엇인가를 넣어준다고 하면 ess equal로 설정을 안 해줘도 되기 때문에 야매긴 하지만 이렇게 해준다.
VS_OUT VS(VertexTextureNormalTangent input)
{
VS_OUT output;
// local -> World -> View -> Proj
float4 viewPos = mul(float4(input.position.xyz, 0), V);//xyzw중 w를 0으로 한뒤 V를 곱하는 거
float4 clipPos = mul(viewPos, P);
output.position = clipPos.xyzw;
output.position.z = output.position.w * 0.999999f;
// xy[-1~1] z[0~1]
// xyzw
// Rasterizer -> w에다가 z값을 보존해서
// x/w y/w z/w 이런 식으로 나눠주는 역할을 한다.
// x/w y/w z/w 1 이렇게 된다.
output.uv = input.uv;
return output;
}
코드를 이렇게 만들면 굉장히 재밌게도 우리가 원하는 바를 이룰 수가 있다는 것이다.
배운 것을 응용하면 얼마든지 새로운 쉐이더를 이렇게 만들어 줄 수가 있다.
#include "00. Global.fx"
#include "00. Light.fx"
struct VS_OUT
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD;
};
VS_OUT VS(VertexTextureNormalTangent input)
{
VS_OUT output;
// local -> World -> View -> Proj
float4 viewPos = mul(float4(input.position.xyz, 0), V);//xyzw중 w를 0으로 한뒤 V를 곱하는 거
float4 clipSpacePos = mul(viewPos, P);
output.position = clipSpacePos.xyzw;
output.position.z = output.position.w * 0.999999f;
// xy[-1~1] z[0~1]
// xyzw
// Rasterizer -> w에다가 z값을 보존해서
// x/w y/w z/w 이런 식으로 나눠주는 역할을 한다.
// x/w y/w z/w 1 이렇게 된다.
output.uv = input.uv;
return output;
}
float4 PS(VS_OUT input) : SV_TARGET
{
float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
return color;
}
technique11 T0
{
pass P0
{
SetRasterizerState(FrontCounterClockwiseTrue);
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, PS()));
}
};
빌드를 해보고 별 다른 문제가 없는지 살펴본다.
결국 VS에서 조작을 해서 강제로 좌표를 조작할 수 있게 되었다가 핵심이다.
3. SkyDemo를 테스트하기 위해 SkyDemo::Init에서 Material과 Object를 만들기
SkyDemo를 테스트하기 위해서는 원래 우리가 했던 방식으로 material을 하나를 다시 만들어 줄텐데
Resources/Textures폴더에 가서 보면 Sky01.jpg라는 파일이 있다.
Material을 로드하는 부분을 SkyDemo::Init에 만들어 보면
// Material
{
shared_ptr<Material> material = make_shared<Material>();
material->SetShader(_shader);
auto texture = RESOURCES->Load<Texture>(L"Sky", L"..\\\\Resources\\\\Textures\\\\Sky01.jpg");
material->SetDiffuseMap(texture);
MaterialDesc& desc = material->GetMaterialDesc();
desc.ambient = Vec4(1.f);
desc.diffuse = Vec4(1.f);
desc.specular = Vec4(1.f);
RESOURCES->Add(L"Sky", material);
}
Sky라는 이름으로 마테리얼을 만들어 줬고,
Sky01을 로드해서 작업을 해줄 것이고,
Object를 하나 파줄 텐데
{
// Object
_obj = make_shared<GameObject>();
_obj->GetOrAddTransform();
_obj->AddComponent(make_shared<MeshRenderer>());
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
_obj->GetMeshRenderer()->SetMesh(mesh);
}
{
auto material = RESOURCES->Get<Material>(L"Sky");
_obj->GetMeshRenderer()->SetMaterial(material);
}
}
_obj라는 이름으로 파주도록 한다.
Mesh는 구형이니까 Sphere로 하고
material은 Sky material을 찾아서 붙인다.
쉐이더는
_shader = make_shared<Shader>(L"18. SkyDemo.fx");
SkyDemo쉐이더를 그대로 붙인다.
void SkyDemo::Init()
{
RESOURCES->Init();
_shader = make_shared<Shader>(L"18. SkyDemo.fx");
// Material
{
shared_ptr<Material> material = make_shared<Material>();
material->SetShader(_shader);
auto texture = RESOURCES->Load<Texture>(L"Sky", L"..\\Resources\\Textures\\Sky01.jpg");
material->SetDiffuseMap(texture);
MaterialDesc& desc = material->GetMaterialDesc();
desc.ambient = Vec4(1.f);
desc.diffuse = Vec4(1.f);
desc.specular = Vec4(1.f);
RESOURCES->Add(L"Sky", material);
}
{
// Object
_obj = make_shared<GameObject>();
_obj->GetOrAddTransform();
_obj->AddComponent(make_shared<MeshRenderer>());
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
_obj->GetMeshRenderer()->SetMesh(mesh);
}
{
auto material = RESOURCES->Get<Material>(L"Sky");
_obj->GetMeshRenderer()->SetMaterial(material);
}
}
// Camera
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());
RENDER->Init(_shader);
}
4. 테스트하기
이 상태에서 실행을 해본다.
회전 키를 눌러 카메라를 돌리면 우주의 다른 면을 볼 수 있다.
하나씩 테스트해본다.
1) perspective 좌표계에서 z값이 1이 될 경우
VS에서
output.position.z = output.position.w * 0.999999f;
0.999999f를 1로 설정하면
깊이 값이 1로 뜬다는 거니까 깊이 테스트에서 이미 버퍼가 1로 밀린 상태가 된다. 똑같은 값이면 안 그리게끔 설정이 되어 있기 때문에 안 그려지고 있는 상태인 것이다.
(보충설명:
output.position.z의 값이 정확히 1로 설정되면, 그 값은 깊이 버퍼의 초기값과 동일하게 됩니다. 깊이 테스트의 기본 설정에 따라, 이미 버퍼에 저장된 값과 새로운 값이 같은 경우, 새로운 픽셀은 화면에 그려지지 않습니다. 이는 보통 "더 가까운 픽셀만 그린다"는 깊이 테스트의 규칙에 따른 것입니다. 따라서, 깊이 값이 1로 설정되면 해당 객체는 깊이 테스트를 통과하지 못해 화면에 그려지지 않게 될 수 있습니다.)
2) View 변환행렬 연산을 스킵할 경우
float4 viewPos = mul(float4(input.position.xyz, 0), V);//xyzw중 w를 0으로 한뒤 V를 곱하는 거
이 부분을 스킵을 하고
float4 viewPos = input.position;
이렇게 input값을 그대로 넣어준다고 하면
회전도 안 하고 world랑 view 연산을 그냥 스킵한다는 얘기가 되는 것이다.
그러면 애당초 물체가 0, 0, 0 위치에 있는 그 방향을 그대로 인지해가지고 월드행렬 변환도 안 하고, 뷰 행렬 변환도 안 할 테니까 실행을 하면 카메라 회전을 해도 고정된 하늘의 이미지가 보이게 된다.
뷰 변환을 스킵하고 float4 viewPos = input.position;으로 직접 설정함으로써, 객체는 카메라와의 상대적인 위치나 방향이 고정된 것처럼 행동한다. 이는 모든 버텍스의 위치가 카메라의 회전이나 이동에 따라 변화되지 않음을 의미한다. 결과적으로, 카메라를 회전시킬 때, 우리는 오브젝트의 다른 면을 볼 수 없게 된다. 대신, 오브젝트는 항상 동일한 방향으로 화면에 표시되며, 카메라의 움직임에 따라 동적으로 변화하는 3D 장면의 일반적인 행동을 하지 않는다. 이로 인해, 마치 오브젝트가 카메라 회전에 동기화되어 같이 회전하는 것처럼 보이지만, 실제로는 카메라의 관점 변화가 오브젝트의 상대적인 위치나 방향에 반영되지 않아서 발생하는 현상이다.
xyzw중 w를 0으로 하고 View연산을 해주었는데, 카메라가 이리저리 돌아가면 그거에 따라 각도가 바뀌어야 되기 때문에 회전값만 인지해서 넣어줄 수 있게끔 세팅을 해준 셈이다.
5. 맺음말
P를 곱해 구하는 clipSpacePos를 구하는 과정을 단계별로 나누면
// local -> World -> View -> Proj
이렇게 된다.
카메라가 바라보는 범위 안의 가두리 양식장 안에 -1~1, 0~1 사이에 있는 값으로 압축하는 게 목표였다.
다만 projection 행렬을 만들어서 곱해준다고 해 가지고 -1~1, 0~1 사이의 비율로 바로 되는 건 아니었었던 이유는 애당초 z값을 바로 나누는 게 아니라 w에 넣어 둬서 rasterizer단계로 넘어갈 때 w에 있는 값으로 xyz를 나눠서 우리가 원했던 -1~1, 0~1 사이 값으로 만드는 게 원래 방식이었는데 그걸 역으로 이용해서 일부로 z에 있는 값을 z값인 w에 거의 근접한 무엇인가로 넣어준 상황이 되는 것이기 때문에 z가 무조건 항상 카메라가 바라보는 최대 범위에 있는 게 된다.
w에 z를 넣는 이유에 대해선 https://devriripong.tistory.com/119 참고한다.
'DirectX' 카테고리의 다른 글
67_인스턴싱_MeshRenderer(인스턴싱) (0) | 2024.03.14 |
---|---|
66_인스턴싱_인스턴싱과 드로우콜 (0) | 2024.03.12 |
64. 애니메이션_애니메이션#4_tweening (0) | 2024.03.08 |
63. 애니메이션_애니메이션#3_shader에서 애니메이션 행렬로 연산, ImGUI로 테스트, 보간 (0) | 2024.03.08 |
62. 애니메이션_애니메이션#2_CreateAnimationTransform, CreateTexture (0) | 2024.03.06 |
댓글