[DirectX11 3D] Landscape Component & LOD

한창희·2024년 6월 10일

DirecX3D 엔진 만들기

목록 보기
13/14

📌LandScape Component

: Tessellation을 이용한 지형 컴포넌트

  • 지형오브젝트의 경우 일반 오브젝트처럼 구현하게되면 매쉬의 디테일을 정하는 데 어려움이 있어, Tessellation을 이용해 Detail을 조절하기 위해 따로 컴포넌트를 만듦
#pragma once
#include "CRenderComponent.h"

class CLandScape :
    public CRenderComponent
{
private:
    UINT            m_FaceX; 
    UINT            m_FaceZ; 

    Ptr<CTexture>   m_HeightMapTex;


public:
    void SetHeightMap(Ptr<CTexture> _HeightMap) { m_HeightMapTex = _HeightMap; }

    
private:
    void Init();
    void CreateMesh();

public:
    virtual void finaltick() override;
    virtual void render() override;
    virtual void UpdateData() override;

public:
    CLONE(CLandScape);
    CLandScape();
    ~CLandScape();
};
  • 가로 세로 면의 개수를 m_FaceX, m_FaceZ에 저장하여, 이 정보를 바탕으로 전용 Mesh를 만든다.
  • HeightMap을 적용하기 위한 m_HeightMapTex 변수를 선언.
#include "pch.h"
#include "CLandScape.h"

#include "CAssetMgr.h"

void CLandScape::Init()
{
	CreateMesh();
}

void CLandScape::CreateMesh()
{
	// 지형 전용 메쉬 생성 및 적용
	vector<Vtx> vecVtx;
	Vtx v;

	// 정점 배치
	for (UINT i = 0; i < m_FaceZ + 1; ++i)
	{
		for (UINT j = 0; j < m_FaceX + 1; ++j)
		{
			v.vPos = Vec3(j, 0.f, i);
			v.vTangent  = Vec3(1.f, 0.f, 0.f);
			v.vNormal   = Vec3(0.f, 1.f, 0.f);
			v.vBinormal = Vec3(0.f, 0.f, -1.f);

			v.vColor = Vec4(1.f, 1.f, 1.f, 1.f);
			v.vUV = Vec2(j, m_FaceZ - i);

			vecVtx.push_back(v);
		}
	}

	// 인덱스 지정
	vector<UINT> vecIdx;

	for (UINT i = 0; i < m_FaceZ; ++i)
	{
		for (UINT j = 0; j < m_FaceX; ++j)
		{
			// 0 
			// | \
			// 2--1
			vecIdx.push_back( (m_FaceX + 1) * (i+1) + j);
			vecIdx.push_back( (m_FaceX + 1) * i + (j + 1));
			vecIdx.push_back( (m_FaceX + 1) * i + j);

			// 1--2
			//  \ |
			//    0
			vecIdx.push_back((m_FaceX + 1) * i + (j + 1));
			vecIdx.push_back((m_FaceX + 1) * (i + 1) + j);
			vecIdx.push_back((m_FaceX + 1) * (i + 1) + j + 1);
		}
	}

	Ptr<CMesh> pMesh = new CMesh;
	pMesh->Create(vecVtx.data(), vecVtx.size(), vecIdx.data(), vecIdx.size());
	SetMesh(pMesh);

	// 지형 전용 재질 적용
	SetMaterial(CAssetMgr::GetInst()->FindAsset<CMaterial>(L"LandScapeMtrl"));
}

Init에서는 면의 개수를 기준으로 정점을 직접 만들어 매쉬를 생성한다.

#include "pch.h"
#include "CLandScape.h"

#include "CTransform.h"

CLandScape::CLandScape()
	: CRenderComponent(COMPONENT_TYPE::LANDSCAPE)
	, m_FaceX(64)
	, m_FaceZ(64)
{
	Init();
}

CLandScape::~CLandScape()
{
}

void CLandScape::finaltick()
{
}

void CLandScape::render()
{
	UpdateData();

	GetMesh()->render();
}

void CLandScape::UpdateData()
{
	Transform()->UpdateData();

	GetMaterial()->SetScalarParam(SCALAR_PARAM::INT_0, m_FaceX);
	GetMaterial()->SetScalarParam(SCALAR_PARAM::INT_1, m_FaceZ);
	GetMaterial()->SetTexParam(TEX_PARAM::TEX_0, m_HeightMapTex);

	GetMaterial()->UpdateData();
}
  • 쉐이더에서도 면의 개수를 알아야하기 때문에 바인딩시키고 나서 랜더링
#ifndef _LANDSCAPE
#define _LANDSCAPE

#include "value.fx"

// =======================================
// LandScape Shader
// MRT      : Deferred
// RS_TYPE  : CULL_BACK
// DS_TYPE  : LESS
// BS_TYPE  : DEFAULT

// Parameter
// g_int_0  : Face X
// g_int_1  : Face Z
// g_tex_0  : HeightMap Texture
// =======================================
struct VS_IN
{
    float3 vPos : POSITION;
    float2 vUV : TEXCOORD;
    
    float3 vTangent : TANGENT;
    float3 vBinormal : BINORMAL;
    float3 vNormal : NORMAL;
};

struct VS_OUT
{
    float3 vPos : POSITION;
    float2 vUV : TEXCOORD;
    
    float3 vTangent : TANGENT;
    float3 vBinormal : BINORMAL;
    float3 vNormal : NORMAL;
};

VS_OUT VS_LandScape(VS_IN _in)
{
    VS_OUT output = (VS_OUT) 0.f;
    
    output.vPos = _in.vPos;
    output.vUV = _in.vUV;
    
    output.vTangent = _in.vTangent;
    output.vBinormal = _in.vBinormal;
    output.vNormal = _in.vNormal;
    
    return output;
}

// Hull Shader
struct PatchLevel
{
    float arrEdge[3] : SV_TessFactor;
    float Inside : SV_InsideTessFactor;
};

PatchLevel PatchConstFunc(InputPatch<VS_OUT, 3> _in, uint patchID : SV_PrimitiveID)
{
    PatchLevel output = (PatchLevel) 0.f;
        
    output.arrEdge[0] = g_vec4_0.x;
    output.arrEdge[1] = g_vec4_0.y;
    output.arrEdge[2] = g_vec4_0.z;
    output.Inside = g_vec4_0.w;
    
    for (int i = 0; i < 3; ++i)
    {
        if (output.arrEdge[i] == 0.f)
            output.arrEdge[i] = 1.f;
    }    
    
    if (output.Inside == 0.f)
    {
        output.Inside = 1.f;
    }    
    
    return output;
}

struct HS_OUT
{
    float3 vPos : POSITION;
    float2 vUV : TEXCOORD;
    
    float3 vTangent : TANGENT;
    float3 vBinormal : BINORMAL;
    float3 vNormal : NORMAL;
};

[patchconstantfunc("PatchConstFunc")]
[outputtopology("triangle_cw")]
[domain("tri")]
[maxtessfactor(64)]
[partitioning("integer")] //[partitioning("fractional_odd")]
[outputcontrolpoints(3)]
HS_OUT HS_LandScape(InputPatch<VS_OUT, 3> _in, uint _idx : SV_OutputControlPointID)
{
    HS_OUT output = (HS_OUT) 0.f;
    
    output.vPos = _in[_idx].vPos;
    output.vUV = _in[_idx].vUV;
    
    output.vTangent = _in[_idx].vTangent;
    output.vBinormal = _in[_idx].vBinormal;
    output.vNormal = _in[_idx].vNormal;
    
    return output;
}

struct DS_OUT
{
    float4 vPosition : SV_Position;
    float2 vUV : TEXCOORD;
        
    float3 vViewPos : POSITION;  
    float3 vViewNormal : NORMAL;    
};



[domain("tri")]
DS_OUT DS_LandScape(PatchLevel _pathlevel // 각 제어점 별 분할 레벨
             , const OutputPatch<HS_OUT, 3> _Origin // 패치 원본 정점
             , float3 _Weight : SV_DomainLocation)   // 각 원본 정점에 대한 가중치)
{
    DS_OUT output = (DS_OUT) 0.f;
        
    float3 vLocalPos = (float3) 0.f;
    float2 vUV = (float2) 0.f;
    
    float3 vTangent = (float3) 0.f;
    float3 vBinormal = (float3) 0.f;
    float3 vNormal = (float3) 0.f;
    
    
    
    for (int i = 0; i < 3; ++i)
    {
        vLocalPos += _Origin[i].vPos * _Weight[i];
        vUV += _Origin[i].vUV * _Weight[i];
        
        vTangent += _Origin[i].vTangent * _Weight[i];
        vBinormal += _Origin[i].vBinormal * _Weight[i];
        vNormal += _Origin[i].vNormal * _Weight[i];
    }
    
    // 높이맵 텍스쳐가 있을 때
    if (g_btex_0)
    {
        float2 FullUV = vUV / float2(g_int_0, g_int_1);
        vLocalPos.y = g_tex_0.SampleLevel(g_sam_0, FullUV, 0).x;
                       
        // 주변 정점(위, 아래, 좌, 우) 로 접근할때의 로컬스페이스상에서의 간격
        float LocalStep = 1.f / _pathlevel.Inside;
        
        // 주변 정점(위, 아래, 좌, 우) 의 높이를 높이맵에서 가져올때 중심UV 에서 주변UV 로 접근할때의 UV 변화량
        float2 vUVStep = LocalStep / float2(g_int_0, g_int_1);
        
        // 위
        float3 vUp = float3( vLocalPos.x
                            , g_tex_0.SampleLevel(g_sam_0, float2(FullUV.x, FullUV.y - vUVStep.y), 0).x
                            , vLocalPos.z + LocalStep);
        
        // 아래
        float3 vDown = float3(vLocalPos.x
                             , g_tex_0.SampleLevel(g_sam_0, float2(FullUV.x, FullUV.y + vUVStep.y), 0).x
                             , vLocalPos.z - LocalStep);
        
        // 좌
        float3 vLeft = float3(vLocalPos.x - LocalStep
                             , g_tex_0.SampleLevel(g_sam_0, float2(FullUV.x - vUVStep.x, FullUV.y), 0).x
                             , vLocalPos.z);
        
        // 우
        float3 vRight = float3(vLocalPos.x + LocalStep
                            , g_tex_0.SampleLevel(g_sam_0, float2(FullUV.x + vUVStep.x, FullUV.y), 0).x
                            , vLocalPos.z);
        
        vTangent = mul(float4(vRight, 1.f), g_matWorld).xyz - mul(float4(vLeft, 1.f), g_matWorld).xyz;
        vBinormal = mul(float4(vDown, 1.f), g_matWorld).xyz - mul(float4(vUp, 1.f), g_matWorld).xyz;      
        vNormal = normalize(cross(vTangent, vBinormal));
        
        output.vViewNormal = normalize(mul(float4(vNormal, 0.f), g_matView).xyz);
    }
    else
    {
        output.vViewNormal = normalize(mul(float4(vNormal, 0.f), g_matWV).xyz);
    }
        
    
    output.vPosition = mul(float4(vLocalPos, 1.f), g_matWVP);
    output.vUV = vUV;    
    output.vViewPos = mul(float4(vLocalPos, 1.f), g_matWV).xyz;    
    
    return output;
}


struct PS_OUT
{
    float4 vColor : SV_Target0;
    float4 vPosition : SV_Target1;
    float4 vNormal : SV_Target2;
    float4 vEmissive : SV_Target3;
};

PS_OUT PS_LandScape(DS_OUT _in) : SV_Target
{
    PS_OUT output = (PS_OUT) 0.f;
        
    output.vColor = float4(0.4f, 0.4f, 0.4f, 1.f);
    output.vPosition = float4(_in.vViewPos, 1.f);
    output.vNormal = float4(_in.vViewNormal, 1.f);
    output.vEmissive = float4(0.f, 0.f, 0.f, 0.f);    
    
    return output;
}



#endif
  • Tessellation을 이용해 정점을 Face 개수에 맞춰 분할해주고, UV를 Face로 나누어 전체 텍스쳐를 기준으로 매핑
  • HeightMap에서 해당 위치의 높이 값을 가져온다.
  • 주변 정점의 높이를 이용해 normal, tangent, binormal을 계산하여 업데이트한다.

📌LOD (Level Of Detail)

: 카메라부터의 거리를 기준으로 테셀레이션의 단계를 낮추거나 높여 성능을 향상시키는 기술

  • 가까이 있는 물체는 더 세밀하게
  • 멀리 있는 물체는 더 간단하게

📌 Static LOD

: 미리 여러단계의 매쉬를 만들어 놓고 카메라와의 거리에 따라서 만들어뒀던 매쉬중 알맞은 매쉬를 사용하는 방법

  • 미리 매쉬를 만들어 놓기 때문에, 동적으로 매쉬를 만들지 않아도 되어서 속도가 빠르다.
  • 물체가 카메라와 가까워질 때, 갑자기 매쉬가 바뀌면서 튀는 듯한 현상이 발생할 수 있다.

📌 Dynamic LOD

: 카메라와의 거리를 계산하여 동적으로 메쉬의 정밀도를 바꿔주는 방법

  • 미리 정해진 메쉬를 사용하는 것이 아니라, 카메라와의 거리에 따라 얼마나 세분화할지를 결정하기 때문에, 카메라와 가까워질때 튀는 현상이 적다.
  • 메쉬를 동적으로 세분화하기 때문에 시간이 더 오래걸린다.

  • 인접한 패치 간의 분할 단계의 차이가 있을 경우 서로의 메쉬가 맞물리지 않으면서 지형이 일부 들리는 현상이 생길수 있다.

  • Tessellation 단계에서 LOD의 기준을 int로 하지 않고 float으로 하여 거리에 비례하여 메쉬를 분할한다면 인접하는 지형간의 차이가 없기 때문에 들림현상이 해결된다.

📌Mipmap

: LOD에 따라 해상도가 다른 텍스쳐를 세팅하는 방법

  • Mipmap을 사용하지 않는다면, 뷰포트에서는 아주 작은 영역에 해당하는 곳도 같은 수준의 텍스쳐를 사용하여 샘플링하게 된다. 이 경우 UV 값이 튀는 현상이 발생해 멀리있는 지형은 노이즈가 생기게 된다.

  • Mipmap을 사용하게 되면 더 많은 텍스쳐를 저장하니 더 많은 메모리를 써야한다고 생각할 수 있다. 하지만 mipmap으로 만들어지는 텍스쳐는 원본 텍스쳐보다 훨씬 작기 때문에 모든 계층을 합친다고 해도 전체 메모리 사용량은 원본 텍스쳐의 크기의 약 1.33배에 불과하다.
    반면에 항상 커다란 텍스쳐를 사용하는것이 아니라, 상황에 맞는 텍스쳐를 사용하기 때문에 GPU의 텍스쳐 캐시를 더 효율적으로 활용할 수 있고, 텍스쳐 로딩시에도 더 빠르다.

  • 결과적으로 Mipmap을 사용하면 텍스처의 전체 메모리 사용량이 약간 증가하지만, 텍스쳐 샘플링 및 핑터링 작업에서 메모리 대역폭과 캐시 효율성을 크게 개선할 수 있고, 이는 렌더링 선응을 향상시키고, 결과적으로 더 나은 프레임 속도와 시각적 품질을 제공할 수 있게 해준다.

0개의 댓글