절두체 컬링(Frustum Culing)이란?

카메라의 시야(절두체)에서 들어오지 않은 객체를 제외하는 것,

즉, 그 객체들은 렌더링에서 제외된다. 하지만 제외작업을 수행하기 위한 연산작업은 필요로 한다.

 

World Space에 배치된 Mesh(객체)들을 <View 변환> 하는 과정

절두체 컬링을 하기 위해서는 World Space에 있는 절두체 영역을(a그림) 구해야된다.

최종 절두체 영역(c그림)의 8개 정점에서 역으로 계산을 해야한다. 

 

Vworld = VLocal * TM(Transfrom Matrix)  :  Vertex data를 월드변환하는 과정의 식

최종Vertex = Vworld * ViewMatrix * ProjectMatix : 월드로 배치된 Vertex를 View(카메라)행렬과 투영행렬을 곱해 카메라를 원점으로 Vertex가 배치되는 식이다.

 

최종Vertex = Vworld * ViewMatrix * ProjectMatirx 식 양변에 카메라 * 투영 역행렬를 곱해주면

최종 Vertex * (카메라 * 투영 역행렬) = Vworld가 나오게 된다. 

( ∵  카메라 * 투영 역행렬 : (ViewMatrix * ProjectMatix)^-1 )

이러면 WorldSpace에 있는 절두체의 영역(우리가 보고있는 카메라 화면)을 찾을수있게된다. 


필요한 구성요소는

절두체 컬링을 하기위해 절두체 class가 필요함 -> Frustum class 를 생성

View행렬과 Matrix행렬을 가져오기 위한 Camera -> Camera 포인터 변수 필요

절두체 면(Plane) 과 벡터의 수직상 거리를 비교할때 필요한 오차범위 -> planeEpsilon;

절두체 면을 관리할 vector -> frustumPlane[6] //밑에 그림의 숫자와 index와 일치

 

8개의 veretx로 6개의 plane을 만들어 절두체 영역을 만들것, 코드영역에서도 이렇게 표현될 예정

Init함수 , 최종Vertex를 구성하는 함수, Frustum 내부에 객체가 있는지 check하는 함수 등등으로 구성하면 될것이다.

 

<Frusutum class> :

벡터(객체의 원점)를 인자값으로 가져와 Frustum 영역의 내부,외부를 체크하여 외부에 있다고 판단시 절두체컬링을 하게하는 class다.

더보기
//Frustum.h

#pragma once
#include "Components/DXCamera.h"

class Frustum
{
private:
	DXCamera* camera; //view,Projection 행렬을 가져오기 위한 변수 

	float planeEpsilon; // 평면테스트에서 사용되는 부동 소수점 오차범위를 나타냄 
	XMVECTOR frustumPlane[6]; //Plane : 평면 -> 절두체 평면 

public:
	bool Initialize(DXCamera* camera, float planeEpsilon = FLT_EPSILON);
	void ConstructFrustum();

	//IsInFrustumBounds~ 함수 설명
    //물체의 기준으로 히트박스(Bounds뒤에 들어가는 도형,도형은 자유) 만들어서 그 안에있으면 있따고 처리 체크 

	bool IsInFrustum(XMFLOAT3 v); //Frustum 안에 물체 확인 bool 변수
	bool IsInFrustum(XMVECTOR v); //Frustum 안에 물체 확인 bool 변수
	bool IsInFrustumExceptUpDown(XMFLOAT3 v); //Frustum의 윗면과 아랫면은 무조건 있는걸로 취급하고 좌우앞뒤만 체크하는 변수

	bool IsInFrustumBoundsSphere(XMFLOAT3 v,float radius); //Frustum 안에 물체의 기준으로 구 모형 안에 있는지 물체 확인 bool 변수
	bool IsInFrustumBoundsSphere(XMVECTOR v, float radius); //Frustum 안에 물체의 기준으로 구 모형 안에 있는지 물체 확인 bool 변수
	bool IsInFrustumBoundsSphereExceptUpDown(XMFLOAT3 v, float radius); //Frustum안에 물체의 기준으로 구 모혀 위와 아래는무조건 있는걸로 취급하고 좌우앞뒤만 체크하는 변수

	bool IsInFrustumBoundsCube(XMFLOAT3 v, float radius); //Frustum 안에 육면체 모양 안에 물체 확인 bool 변수
	bool IsInFrustumBoundsCube(XMVECTOR v, float radius); //Frustum 안에 육면체 모양 안에 물체 확인 bool 변수

	//Rectangle : AABB/OBB -> 2D게임 만들때거나 3D게임의 UI 만들때 사용
	bool IsInFrustumBoundsRectangle(XMFLOAT3 v, XMFLOAT3 r); //Frustum 안에 2D사각형 안에 물체 확인 bool 변수
	bool IsInFrustumBoundsRectangle(XMVECTOR v, XMVECTOR r); //Frustum 안에 2D사각형 안에 물체 확인 bool 변수

private:
	bool IsInBoundsCube(XMVECTOR p, XMFLOAT3 v, XMFLOAT3 r);
};

//====================================================================

#include "Frustum.h"
bool Frustum::Initialize(DXCamera* camera, float planeEpsilon)
{
	if (!camera) return false; // 매개변수 camera 가 존재하지 않으면 false 반환

	//Frustum data 초기화
	this->camera = camera;
	this->planeEpsilon = planeEpsilon;
	for (int i = 0; 6 > i; i++) frustumPlane[i] = XMVectorSet(0, 0, 0, 0);
	return true;
}

void Frustum::ConstructFrustum()
{
	//최종 vertex는 (-1,-1,0) ~ (1,1,1) 사이의 값으로 바뀐다.
	//최종Vertex = Vworld * ViewMatrix * ProjectMatirx해서 나온 Frusutm 영역의 8개 vertex data임.
    XMFLOAT3 frustumVertices[8] =
	{
		{-1.0f, -1.0f, 0.0f }, // near left bottom v0
		{1.0f, -1.0f, 0.0f }, // near right bottom v1
		{1.0f, -1.0f, 1.0f }, // far right bottom v2
		{-1.0f, -1.0f, 1.0f }, // far left bottom v3
		{-1.0f, 1.0f, 0.0f }, // near left top v4
		{1.0f, 1.0f, 0.0f }, // near right top v5
		{1.0f, 1.0f, 1.0f }, // far right top v6
		{-1.0f, 1.0f, 1.0f }, // far left top v7
	};

	//view * proj의 역행렬 계산.
	XMMATRIX invViewProjectionMatrix = XMMatrixInverse(NULL, camera->GetViewMatrix() * camera->GetProjectionMatrix());
	
	XMVECTOR frustumVectors[8]; //최종vertex * (viewprojectionMatrix)T를 계산한 vertex들의 vector
	for (int i = 0; 8 > i; i++)
	{
		// XMVector3TransformCoord : 주어진 행렬을 사용하여 벡터로 변형하는 함수
		//최종vertex * (viewprojectionMatrix)T 계산,
		frustumVectors[i] = XMVector3TransformCoord(XMVectorSet(frustumVertices[i].x, frustumVertices[i].y, frustumVertices[i].z, 1.0f),
			invViewProjectionMatrix);
	}
	
    //계산된 Vworld(월드상의 vertex)들을 3개씩 사용하여 절두체의 평면을 구성 
    //인덱스 순서에 따라 절두체면(폴리곤)의 윗면이 결정되어 윗면이 법선벡터의 방향이다.(중요)
	frustumPlane[0] = XMPlaneFromPoints(frustumVectors[4], frustumVectors[7], frustumVectors[6]); // 상 평면(top)
	frustumPlane[1] = XMPlaneFromPoints(frustumVectors[0], frustumVectors[1], frustumVectors[2]); // 하 평면(bottom)
	frustumPlane[2] = XMPlaneFromPoints(frustumVectors[0], frustumVectors[4], frustumVectors[5]); // 근 평면(near)
	frustumPlane[3] = XMPlaneFromPoints(frustumVectors[2], frustumVectors[6], frustumVectors[7]); // 원 평면(far)
	frustumPlane[4] = XMPlaneFromPoints(frustumVectors[0], frustumVectors[3], frustumVectors[7]); // 좌 평면(left)
	frustumPlane[5] = XMPlaneFromPoints(frustumVectors[1], frustumVectors[5], frustumVectors[6]); // 우 평면(right)
}


bool Frustum::IsInFrustum(XMFLOAT3 v)
{
	return IsInFrustum(XMVectorSet(v.x, v.y, v.z, 1.0f));
}
bool Frustum::IsInFrustum(XMVECTOR v)
{
	return IsInFrustumBoundsSphere(v, 0.0f);
}
bool Frustum::IsInFrustumExceptUpDown(XMFLOAT3 v)
{
	return IsInFrustumBoundsSphereExceptUpDown(v, 0.0f);
}
bool Frustum::IsInFrustumBoundsSphere(XMFLOAT3 v, float radius)
{
	return IsInFrustumBoundsSphere(XMVectorSet(v.x, v.y, v.z, 1.f), radius);
}

//절두체 안에 경계구가 있는지 체크하는 함수( 작동방식 밑에 서술)
bool Frustum::IsInFrustumBoundsSphere(XMVECTOR v, float radius)
{
	for (int i = 0; 6 > i; i++) // 왜 6인가? 절두체의 면이 6개임
	{
		//XMPlaneDotCoord의 함수 반환값은 XMVECTOR인데, XMVECTOR의 모든값을 내적크기로 반환한다.
		//VectorGetX,Y,Z 함수 아무거나 사용해도 괜찮다. 전부 내적의 값이기 떄문이다.
		if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], v)) > (radius + planeEpsilon))
		{
			// 벡터의 내적(물체와 절두체 면의 수직상의 거리) > 물체의 경계(히트박스)거리 가 참인경우
			return false; //frustum 영역 외부에 있음
		}
	}

	return true;//frustum 영역 내부에 있음
}

bool Frustum:: IsInFrustumBoundsSphereExceptUpDown(XMFLOAT3 v, float radius)
{
	XMVECTOR vector = XMVectorSet(v.x, v.y, v.z, 1.0f);

	for (int i = 2; 6 > i ; i++) // 0,1 인덱스의 vertex는 윗면과 ,아랫면의 vertex이므로 제외
	{
		//XMPlaneDotCoord (평면(plane) , 벡터(vertex) ) : 평면과 벡터사이의 내적값을 반환.
		if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], vector)) > (radius + planeEpsilon)) return false;
	}

	return true;
}

//절두체를 경계박스로 컬링하는 함수 
bool Frustum::IsInFrustumBoundsCube(XMFLOAT3 v, float radius)
{
	for (int i = 0; 6 > i; i++)
	{
		if (IsInBoundsCube(frustumPlane[i], v, XMFLOAT3(radius, radius, radius))) continue;
		return false;
	}
	return true;
}

bool Frustum::IsInFrustumBoundsCube(XMVECTOR v, float radius)
{
	XMFLOAT3 dest;
	XMStoreFloat3(&dest, v);
	return IsInFrustumBoundsCube(dest, radius);
}

//절두체를 2D사각형으로 컬링하는 함수(2D게임에서 사용하게됨)
bool Frustum::IsInFrustumBoundsRectangle(XMFLOAT3 v, XMFLOAT3 r)
{
	for (int i = 0; 6 > i; i++)
	{
    	//
		if (IsInBoundsCube(frustumPlane[i], v, r)) continue;
		return false;
	}
	return true;
}

bool Frustum::IsInFrustumBoundsRectangle(XMVECTOR v, XMVECTOR r)
{
	XMFLOAT3 destV, destR;
	XMStoreFloat3(&destV, v);
	XMStoreFloat3(&destR, r);
	return IsInFrustumBoundsRectangle(destV, destR);
}

//경계박스 ,및 경계사각형(2D)의 함수 작동방식 
bool Frustum::IsInBoundsCube(XMVECTOR p, XMFLOAT3 v, XMFLOAT3 r)
{
	return
		(
        	//절두체 면(p) ,벡터v,r연산
            //모든 벡터의 내적이 오차범위 보다 작으면 있면 frustum 안에 존재 
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y - r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y - r.y, v.z + r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y + r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y + r.y, v.z + r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y - r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y - r.y, v.z + r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y + r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y + r.y, v.z + r.z, 1.0f))) <= planeEpsilon)
		);
}

 

최종 Vertex의 값 범위가 (-1,-1,0) ~ ( 1,1,1) 사이의 값으로 바뀐다 라는 주석이 있는데

이 값은 D3D에서는 절대 고정이며 view 변환시 카메라 시점을 원점으로 바꾸고 투영행렬을 계산하여 이미지화 (3D->2D)화 시키기 때문에 그런것이다.

//최종 vertex는 (-1,-1,0) ~ (1,1,1) 사이의 값으로 바뀐다.
	//최종Vertex = Vworld * ViewMatrix * ProjectMatirx해서 나온 Frusutm 영역의 8개 vertex data임.
    XMFLOAT3 frustumVertices[8] =
	{
		{-1.0f, -1.0f, 0.0f }, // near left bottom v0
		{1.0f, -1.0f, 0.0f }, // near right bottom v1
		{1.0f, -1.0f, 1.0f }, // far right bottom v2
		{-1.0f, -1.0f, 1.0f }, // far left bottom v3
		{-1.0f, 1.0f, 0.0f }, // near left top v4
		{1.0f, 1.0f, 0.0f }, // near right top v5
		{1.0f, 1.0f, 1.0f }, // far right top v6
		{-1.0f, 1.0f, 1.0f }, // far left top v7
	};

 

 

 

 

절두체 각 면의 법선벡터 방향을 정하는 것은

그래픽스 파이프라인 폴리곤을 그리는 방식에 따라 정해진다.

이 블로그의 그래픽스 파이프라인 - 기본용어 정리 - Vertex를 이용하여 삼각형을 그릴때 필요한 개념 내용을 참고하면 좋다.

https://01149.tistory.com/4

 

기본용어 정리

글 작성자가 공부하고 배운것을 간략하게 정리하는 페이지 입니다. 필요한 정보가 있으시면 확인하시고 만약 잘못된 내용이 있거나 이런 내용이 추가됬으면 좋겠다라면 댓글 부탁드립니다. 감

01149.tistory.com

 

절두체 컬링의 경계 구 방식의 작동방식을 설명 할 것이다.

bool Frustum::IsInFrustumBoundsSphere(XMVECTOR v, float radius)
{
	for (int i = 0; 6 > i; i++) // 왜 6인가? 절두체의 면이 6개임
	{
		//내적은 frustumPlane에서 v까지의 수직상 거리와 같다.
		//벡터의 내적의 크기(벡터 * 법선벡터 = 해당면으로 수직의 벡터크기(길이) 
		// {벡터의 내적 < 오브젝트의 구 범위(반지름이 더 크면)} frustum안에 있다는 뜻이다.
		// XMPlaneDotCoord의 함수 반환값은 XMVECTOR인데, XMVECTOR의 모든값을 내적크기로 반환하기 떄문이다.
		// VectorGetX,Y,Z 함수 아무거나 사용해도 괜찮다. 전부 내적의 값이기 떄문이다.
		if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], v)) > (radius + planeEpsilon))
		{
			// 벡터의 내적(물체와 절두체 면의 수직상의 거리) > 물체의 경계(히트박스)거리 가 참인경우
			return false; //frustum 영역 외부에 있음
		}
	}

	return true;//frustum 영역 내부에 있음
}
절두체 컬링 - 경계구 방식의 작동 방식

위의 그림 과정을 절두체 면이 6개니 총 6번 반복하여 동작을한다.

if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], v)) > (radius + planeEpsilon))
{
    return false;
}

반복문 동작중 절두체 6면 중 한 면이라도,

벡터의 내적(수직상의 거리) 이 radius + planeEpsilon (경계 구 반지름 + 오차범위)보다 길면
frustun 외부에 존재하는 것이기 때문에 절두체 컬링(화면에서 렌더링을 제외하는)을 작업하게 된다.

<DirectX11Graphics> : DriectX 11 기반으로 Scene을 렌더링하기 위한 그래픽스 객체

더보기
// DirectX11Graphics.h
#include "Frustum.h"
class DirectX11Graphics final : public Graphics
{
Private:

	// Mesh의 이미지 관련 함수들을 담당하고 있음
    std::unique_ptr <PrimitiveModel> primitive; 
    // 컴포넌트 구조의 Mesh Obj : 컴포턴트 추가 및 Update관련(Mesh의 크기,회전,이동 함수등) 
	DXGameObject primitiveObj; 

	Frustum frustum; //절두체컬링을 위한 절두체 객체 추가
};

//========================================================

// DirectX11Graphics.cpp

void DirectX11Graphics::RenderFrame() //그래픽스 객체의 Draw 함수(프레임마다 렌더링함수)
{
	//렌더링 할때마다 , 카메라시점 기준(뷰행렬,투영행렬)으로 절두체 영역 생성
	frustum.ConstructFrustum();

    // GameObject Primitive Mesh Draw
    //절두체 컬링 - 경계구 판정을 사용하여 Mesh의 위치vector와 크기Vector를 인자값으로 가져감
    //크기vector는 경계구의 반지름으로 사용, 해당 객체가 카메라영역에서 사라지고 경계구까지 영역에서 벗어나게되면 절두체 컬링진행
   	//(이 자리에 경계 구의 반지름 값을 조정하여 넣을 수있음 )
    if (frustum.IsInFrustumBoundsSphere(primitiveObj.GetTransform()->GetPosition(), XMVectorGetX(primitiveObj.GetTransform()->GetScale())/*bounds*/))
    {
    	//절두체 6면, 영역 내에 Mesh(객체)가 존재 함으로 Draw를 한다.
    	primitiveObj.Draw(cameraObj.GetComponent<DXCamera>()->GetViewProjectionMatrix());
	}
}


bool DirectX11Graphics::InitializeScene() // 그래픽스 객체의 Scene(장면) Init
{
	//상자 mesh
    Primitive = std::make_unique<PrimitiveCube>(); // 상자 모양 mesh
    Primitive->Initialize(device.Get(), deviceContext.Get(), constantMatricesBuffer);
    Primitive->MakePrimitive("Textures\\box.jpg"); // mesh에 텍스쳐 이미지 추가
    PrimitiveObj.AddComponent<DXMeshRenderer>()->SetModel(Primitive.get());
    
    //절두체 객체 Init , 카메라의 뷰행렬,투영행렬이 필요하기 때문 -> 역행렬을 구해야함
	frustum.Initialize(cameraObj.GetComponent<DXCamera>());

}

 

D3D 공부하던 코드를 긁어온거입니다. 코드 보실때 참고만해주세요 


3D 공간에서 기존 카메라의 영향을 받지 않는 2D Obj를 만든다.

즉, D3D를 이용하여 2D 게임을 만들거나, 게임의 UI를 구성할때 많이 사용된다.

 

Sprite2D의 구성요소는 

유저에게 보여줘야할 2D Obj -> sprite

2D를 비춰줄 전용 카메라 Obj -> camera2D

2D를 이미지를 렌더링해줄 컴포넌트 ->SpriteRenederer

2D에 사용할 전용 VS셰이더와 PS셰이더 -> sprite_vertexShader, sprite_pixelShader

셰이더를 사용하면 필요한 HLSL -> sprite2D_ps.hlsl,sprite2D_vs.hlsl (따로작성)

 

<HLSL 작성>

더보기
//sprite2D.ps.hlsl

struct PixelInput
{
	float4 position : SV_POSITION;
	float4 color : COLOR;
	float2 texcoord : TEXCOORD0;
}; //빛의 영향을 받지 않기 때문에 법선벡터 data를 가질필요가 없다.

Texture2D tex : TEXCOORD: register(t0);
SamplerState sample : SAMPLER: register(s0);
float4 main(PixelInput input) : SV_TARGET
{
	return input.color * tex.Sample(sample, input.texcoord);
}

//======================================================

// sprite2D.vs.hlsl

#pragma pack_matrix(row_major)
cbuffer constants
{
	matrix worldMatrix;
	matrix viewProjectionMatrix;
};
struct VertexInput
{
	float3 position : POSITION;
	float4 color : COLOR;
	float2 texcoord : TEXCOORD0;
	float3 normal : NORMAL;
};
struct VertexOutput
{
	float4 position : SV_POSITION;
	float4 color : COLOR;
	float2 texcoord : TEXCOORD0;
}; // ps.hlsl의 input과 vs.hlsl의 output은 똑같아야한다.(중요)

VertexOutput main(VertexInput input)
{
	VertexOutput output;
	output.position = mul(float4(input.position, 1.0f), worldMatrix);
	output.position = mul(output.position, viewProjectionMatrix);
	output.color = input.color;
	output.texcoord = input.texcoord;
	return output;
}

<SpriteRenderer 생성(컴포넌트)>

더보기
//SpriteRenderer class 생성 - 컴포넌트로 사용

// DXSpriteRenderer.h
#pragma once
#include "../DXGameObject.h"
#include "../Model.h"
class DXSpriteRenderer : public RenderComponent
{
    private:
        std::unique_ptr<Mesh> mesh; //그림을 그릴 mesh
        
        ID3D11DeviceContext* deviceContext;
        ConstantBuffer<Matrices>* vsConstantBuffer; //VertexShader에서 사용할 행렬data
    public:
        DXSpriteRenderer(DXGameObject* owner); // Render컴포넌트를 자기자신으로 선언 하는 생성자
        
        //sprite이미지를 로드해서 Mesh에 초기화
        void LoadSpriteImage(ID3D11Device* device, ID3D11DeviceContext* deviceContext, ConstantBuffer<Matrices>& vsConstantBuffer, const std::string& filename);
        
    protected:
    	virtual void Opertate() override; 
};

//=========================================

// DXSpriteRenderer.cpp

#include "DXSpriteRenderer.h"
DXSpriteRenderer::DXSpriteRenderer(DXGameObject* owner) :
    RenderComponent(owner),
    deviceContext(nullptr),
    vsConstantBuffer(nullptr)
{
}


void DXSpriteRenderer::LoadSpriteImage(ID3D11Device* device, ID3D11DeviceContext* deviceContext, ConstantBuffer<Matrices>& vsConstantBuffer, const std::string& filename)
{
	//2DspriteRenderer의 deviceContext,vsConstantBuffer 초기화
	this->deviceContext, = deviceContext;
	this->vsConstantBuffer = &vsConstantBuffer;
	
    //vertex 벡터 초기화
	std::vector<ModelVertex> vertices =
	{
		ModelVertex(-0.5f, -0.5f, 0.0f,0.0f, 0.0f), // top left
		ModelVertex(0.5f, -0.5f, 0.0f,1.0f, 0.0f), // top right
		ModelVertex(-0.5f, 0.5f, 0.0f,0.0f, 1.0f), // bottom left
		ModelVertex(0.5f, 0.5f, 0.0f,1.0f, 1.0f), // bottom right
	};
    //index 벡터 초기화 
	std::vector<DWORD> indices =
	{
		0, 1, 2,
		2, 1, 3
	};
    
    //인자값으로 가져온 텍스쳐 파일이름을 Texture생성자에서 2D이미지 로드
	std::vector<Texture> textures = { Texture(device, filename) };
	//mesh에 data를 초기화해서 모델링하는 작업 
    mesh = std::make_unique<Mesh>(device, deviceContext, vertices, indices, textures, XMMatrixIdentity());
}

//constantBuffer = 정점 및 픽셀 셰이더에서 사용될 상수를 모아 놓은 버퍼이다.
//ConstantBuffer에 월드행렬과 뷰투영 행렬을 초기화 하고 메모리락(ApplyChanges) 한 뒤 mesh를 Draw한다. 
void DXSpriteRenderer::Opertate()
{	
	deviceContext->VSSetConstantBuffers(0, 1, vsConstantBuffer->GetAddressOf());
	vsConstantBuffer->data.world = mesh->GetTransformMatrix() * owner->GetTransform()->GetWorldMatrix();
	vsConstantBuffer->data.viewProjection = viewProjectionMatrix;
	vsConstantBuffer->ApplyChanges();
	mesh->Draw();
}

<DirectX 11 Graphics 의 2D sprite 추가>

: DirectX 11 Graphics : DirectX 11 기반으로 씬(Scnen)을 렌더링하기 위한 그래픽스 객체

더보기
// DirectX11Graphics.h

#include "Components/DXSpriteRenderer.h" //SpriteReneder 헤더 추가
class DirectX11Graphics final : public Graphics
{
Private:
	...
    VertexShader sprite_vertexShader; //2Dsprite VS
    PixelShader sprite_pixelShader; //2Dsprite PS
    DXGameObject camera2D; //2D 전용 카메라 Obj
    DXGameObject sprite; //2D 이미지 mesh
	...
};

//=================================

// DirectX11Graphics.cpp

void DirectX11Graphics::RenderFrame()
{
	...
    //deviceContext에 sprite관련 data를 Set(IA,VS,PS)
    deviceContext->IASetInputLayout(sprite_vertexShader.GetInputLayout());
    deviceContext->VSSetShader(sprite_vertexShader.GetShader(), NULL, 0);
    deviceContext->PSSetShader(sprite_pixelShader.GetShader(), NULL, 0);
    //Camera2D에 뷰투영행렬을 인자값으로 가져와서 draw를 실행
    sprite.Draw(camera2D.GetComponent<DXCamera>()->GetViewProjectionMatrix());
    swapChain->Present(NULL, NULL);
}

//Shader Init
bool DirectX11Graphics::InitializeShader()
{
   ...
    //vs,ps HLSL Initialize
    if (!sprite_vertexShader.Initialize(device.Get(), L"sprite2D.vs.hlsl", inputLayout,
    numElements)) return false;
    if (!sprite_pixelShader.Initialize(device.Get(), L"sprite2D.ps.hlsl")) return false;
    return true;
}

//Scene Init
bool DirectX11Graphics::InitializeScene()
{

...
		//Cam(Base컴포넌트)에 DXCamera(컴포넌트)추가
		DXCamera* cam = camera2D.AddComponent<DXCamera>();
        //Cam의 투영방법 Set : 직교 투영
		cam->SetProjectionType(ProjectionType::Orthographic);
		//카메라 좌표계 원점을 left,top에서 중앙으로 옮기는 과정 
		cam->SetProjection(screenWidth, screenHeight, -10.0f, 10.0f);

		//2D 이미지(sprite)dp Renderer 컴포넌트 추가후 이미지 로드
		sprite.AddComponent<DXSpriteRenderer>()->LoadSpriteImage(
			device.Get(), deviceContext.Get(), constantMatricesBuffer,
			"Textures\\hello_world_sprite.jpg");

		//2D이미지는 Scale 및 Translation 조정시 3D처럼 원본의 배율이 아닌 직접 값을 입력해야된다. 
		//sprite.GetTransform()->SetLocalScale({ 100.0f, 100.0f, 1 }); // Scale Set
		//sprite.GetTransform()->SetLocalRocation({ 0, 0, 30.0f }); // Rotation Set(Z축을 회전시켜야 2D이미지가 회전됨)
        //sprite.GetTransform()->SetTranslation({ 100.0f, 100.0f, 0 }); // Translation Set
        /**/


...
}

 

2D 카메라 세팅중 직교투영과 좌표계 수정이 필요하다.

bool DirectX11Graphics::InitializeScene()
{        ...
        //Cam의 투영방법 Set : 직교 투영
		cam->SetProjectionType(ProjectionType::Orthographic);
		//카메라 좌표계 원점을 left,top에서 중앙으로 옮기는 과정 
		cam->SetProjection(screenWidth, screenHeight, -10.0f, 10.0f);
        ...
}

 [2D카메라 투영 세팅]

//DXCamera 투영 행렬을 Set 하는 함수
void DXCamera::SetProjection(float width, float height, float nearZ, float farZ, float fovDegrees)
{
	this->width = width;
	this->height = height;
	this->nearZ = nearZ;
	this->farZ = farZ;
	this->fovDegrees = fovDegrees;

	switch (type)
	{ // 원근/직교(Perspective/Orthographic) 투영 행렬 생성.
	case ProjectionType::Perspective: projectionMatrix = XMMatrixPerspectiveFovLH(fovDegrees * Deg2Rad, (width / height), nearZ, farZ); break;
	case ProjectionType::Orthographic: projectionMatrix = XMMatrixOrthographicOffCenterLH(-width / 2, width / 2, height / 2, -height / 2, nearZ, farZ); break;
	}
}

DXCamera 객채 생성자에서 원근투영(Perspective)으로 set하게 해놨기 떄문에 직교투영(Orthographic)으로 set해줘야한다.

 

-원근투영

뷰투영행렬 연산을 거쳐 카메라 화면(유저가 보는 화면 = viwe port)이 모니터에 출력되는데

이때 데카르트 좌표계(정규좌표)를 사용하고 있다.

우리가 흔히 쓰는 데카르트 좌표계가 기준

 그렇기 때문에 원점(0,0)이 Width,Height 값만 있으면 원점은 정중앙으로 인식된다.

XMMatrixPerspectiveFovLH(fovDegrees * Deg2Rad, (width / height), nearZ, farZ); 

view Space의 각도(=카메라의 각도) , (카메라 화면 폭 / 카메라 화면 높이) , 가까운 Z축 plane , 멀리있는 Z축 plane

 

-직교투영

원근투영과는 다르게 직교투영은 화면좌표계(컴퓨터좌표계)를 사용한다.

원점(0,0)이 정중앙이 아닌 left,top으로 되있기 때문에 쉽게 사용하라면 원근투영처럼 원점을 정중앙으로 옮겨줘야 된다.

Y축의 증감 방향이 반대로 되어있다.

XMMatrixOrthographicOffCenterLH(-width / 2, width / 2, height / 2, -height / 2, nearZ, farZ);  

width와 height의을 절반으로 나눠 -,+값으로하면 원하는 카메라 화면을 구할수 있다.

 

 

계층구조는 이 블로그를 참고하여 작성하였습니다. 

5장 - 1 에서는 계층구조의 종류 및 계층구조 구현(행렬 계산)에 대하여 작성되어있고,

5장 - 2 에서는 계층구조를 코드를 이용하여 구현하는것을 작성되어있다. 

https://blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sipack7297&logNo=220427434575 

 

[D3D] 5장 : 계층구조(Hierarchy)_1

계층구조(Hierarchy) 물체들의 대부분이 관절체(articulated body)로 이루어져 있다. 이러한 관절체의 가...

blog.naver.com

https://blog.naver.com/sipack7297/220428447625

 

[D3D] 5장 : 계층구조(Hierarchy)_2

계층구조 실제 구현 처음에는 해골책에 있는 소스를 활용해서  지구와 달과의 관계를 시뮬레이션을 해...

blog.naver.com

 


계층구조(Hierarchy)

더보기

물체의 대부분이 관절체(Articulated Body)로 이루어져있다.

관절체의 큰 특징은 부모-자식 관계를 갖는 구조로 이루어져 있으며 이를 계층구조라 한다.

(상속 개념 X)

자료구조의 트리(Tree)를 이용하여 구현이 가능하다. 

부모 노드(Node) -자식 노드(Node)로 계층구조가 이루어져 있다. 

즉, 계층 구조는 부모-자식 관계를 3D로 구현할떄 사용된다. 

 

계층구조의 예시 - 사람

그림 출처 : https://blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sipack7297&logNo=220427434575

왼쪽 그림은 인간형태로 모델링된 메시, 오른쪽 그림은 인간모델의 계층구조를 도식화 한것

트리 구조로 보자면 허리(코어)가 최상위 부모(Root Node)가 되고 허리의 자식노드들이 가슴(상반신)과 엉덩이(하반신)이 되고 이것이 점점 아래로 내려가면서 트리 구조를 구현한다.

 

p.s 
상속이 계층구조를 띄는거지 계층구조가 상속인것은 아니다.
컴퓨터 그래픽스의 계층구조는 tree자료구조인 nood로 연결되어있는 구조(중요) 

 

계층구조의 구현 (행렬 계산)

더보기

계층 구조 구현은 행렬의 계산이므로 vertex들을 월드매트릭스로 옮기는 과정을 똑같이 진행하면 된다.

행렬 곱 계산시 순서(위치)가 매우 중요하므로 , 순서는 반드시 이 순서대로만 해야된다. (중요)

 

TransformMatrix(변환행렬) = scaleMatrix * rotationMatrix * translationMatrix

정점 변환 (월드) : Vworld = Vlocal * TM(TransformMatrix)

계층구조의 정점 (자식노드의 메쉬 구현) : Vworld =  Vlocal * TMChild * TMParent

 

Vlocal : Local Space에 배치되어있는 Vertex

(Local/Model Space : 3D 오브젝트 모델을 처음 생성할 때에 모델을 가장 편리한 방법으로 배치 / 편리한 방법 : 자기 자신을 원점으로 잡음)

 

계층구조의 정점 구현은 LocalSpace의 자신(Child)의 vertex들이 배치되어있고 TM을 곱하여 월드 매트릭스로 변환 하고 부모의 변환행렬을 곱하여 자신(Child) LocalSpace의 기준을 부모의 원점을 기준으로 바꾸어 구현한다.

즉, 계층구조의 정점 구현시 LocalSpace가 자기 자신이 아닌 부모의 원점을 기준으로 구현하게 된다. 

 

 

계층구조는 n개이상으로 구현이 되면 , 부모TM들은 내 위에 부모노드 들을 순서에 유의하며  n개를 전부 곱한다.

n개 계층 표현식 : Vworld = Vlocal * TMChild * TMParent(n) * TMParent(n-1) * ... * TMParent(1) 

 

그림 출처 : https://blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sipack7297&logNo=220427434575

예를 들어 오른발(foot_r) 메시가 구현(렌더링)되기 위해서는 다음과 같은 과정을 거친다. 

Vworld =

Vx3ds_foot_r(오른발의 Local Vertex)

* TMx3ds_foot_r (오른발 변환행렬)

* TMx3ds_leg_dr(오른 다리 변환행렬)  

* TMx3ds_leg_ur(오른 허벅지 변환 행렬) 

* TMx3ds_hip(엉덩이 변환행렬) 

* TMx3ds_waist(허리 변환행렬)

 

 

 

밑에 부분은 그냥 내가 공부했던 내용중에 있어서 기입했다. 

p.s 부모 노드들을 다 곱해야되는가?

 꼭 그럴필요는 없다. 다 곱하게되면 세밀한 표현 및 정확한 위치를 얻을수 있지만, 구현 및 처리비용에 중점을 두게 된다면 구현시에 LocalVertex를 mesh를 그릴때 연산해주면된다.

즉, TM을 WorldMatrix라고 지칭하고, mesh를 렌더링(draw)하는 구간에 VLocal(Local vertex)를 연산해주면 월드에 구현이 된다. 

//예시) 오른 다리의 부모TM을 다 곱했다고 가정 

//오른 다리 구현
WorldMatrix = ScaleM * RotationM * TranslationM ;//월드 매트릭스 = TM 
//오른 다리 draw
Vworld_3ds_leg_dr  = Vx3ds_leg_dr * WorldMatrix * WorldMatrix_parent; // WorldMatrix_parent : 오른 다리의 부모TM의 곱
▼
//오른 발 구현 
WorldMatrix = ScaleM * RotationM * TranslationM ;//월드 매트릭스 = TM 
//오른 발 draw
Vworld_x3ds_foot_r = Vx3ds_foot_r * WorldMatrix  * WorldMatrix_parent; // WorldMatrix_parent : 오른 발의 부모TM의 곱(오른다리가 포함되어있음)

 

<코드 영역>

더보기

※ 공부중인 코드 일부분을 긁어온거라 참고용으로만 봐주세요.


<지구 자전>

//DirectX11Graphics.h
class DirectX11Graphics 
{
private:
	...
    
    DXGameObject cameraObj; // 카메라 Obj
    
   	// PrimitiveModel은 mesh data와 vertex shader에서 사용할 행렬 data가 담겨있다.
    // 즉, Textuer를 렌더링하는 data를 담당하는 변수 
	std::unique_ptr <PrimitiveModel> Earth; 
   
    //Component 구조로 설계한 DXGameObject를 사용
    //Transform으로 크기행렬,회전행렬,이동행렬 수정 및 Set할때 사용됨 
    //RenderComponent만 추가하여 Render 할때 사용되는 변수 
	DXGameObject EarthObj; 

    
    ...
}

//DirectX11Graphics.cpp
void DirectX11Graphics::Update() // update 영역에서 지구를 자전시키는 코드를 생성
{
	...
    
    // timer class를 만들어서 델타타임을 활용 하게 만듬
	const float deltaTime = timer->GetMilisecondsElapesed();
	static float earthYaw = 0;
	
    earthYaw += deltaTime * 0.1f;
	EarthObj.GetTransform()->SetLocalRotation({ 0,earthYaw, 0 });  // 지구의 y축 회전
	
    ...
}

bool DirectX11Graphics::InitializeScene()
{
	...
    
    //지구
    Earth = std::make_unique<PrimitiveSphere>(); //mesh를 구 모양으로 초기화 
    Earth->Set_size(3); //지구의 scale 크기 Set
    Earth->Initialize(device.Get(), deviceContext.Get(), constantMatricesBuffer);// Earth Model Init
    Earth->MakePrimitive("Textures\\earth2048.bmp"); // mesh에 텍스쳐 이미지 추가
    EarthObj.AddComponent<DXMeshRenderer>()->SetModel(Earth.get()); //Renderer component를 추가후 Earth Mesh를 저장
    
    ...
}

void DirectX11Graphics::RenderFrame() 
{
	...
    //GameObject Primitive Mesh Draw
    //world space에 배치된 obj를 뷰투영행렬을 연산해 카메라시점으로 변환
    //인자값으로 카메라obj에 있는 뷰투영행렬을 Get
	EarthObj.Draw(cameraObj.GetComponent<DXCamera>()->GetViewProjectionMatrix());
    
    swapChain->Present(NULL, NULL);
}

코드 작성후 빌드를 하면

 

이런식으로 지구가 자전을 하는 모습을 볼 수 있다.

 

이제 계층구조를 활용하여 간단한 태양계를 만들어 볼건데, 

일반적으로 태양-지구-달 계층구조를 생각하면 왼쪽을 생각할 것이다.

이 그림의 의미는 계층구조를 구성시 부모-자식 관계가 꼭 Mesh 들로만 구성을 하지 않아도 된다는것이다.
(중요한 포인트)
왼쪽 처럼 구성을 한 경우 화면에 보여지는건 정상적으로 움직일 것이다.

하지만 

태양의 자전을 멈추게하면 지구는 공전을 하지 않게 된다.
과학적으로는 태양의 자전으로 인해 지구가 공전 이런 내용이 맞다 하지만, 

내가 사용하고싶은건 태양은 자전을 하지 않지만 지구는 공전을 하는것이 의도면 왼쪽처럼 구성하면 안된다.

 

만약 태양과 지구가 직접 node로 설계되어있다면

지구Vworld = 지구Vlocal * 지구TM * 태양TM 이 변환식일것이다.
이러면 태양 TM의 크기,회전,이동 행렬이 전부 지구에 직접적으로 연관을 주게 되어 태양이 자전이 멈추게 되면 
지구도 공전을 하지 않게 된다.

 

하지만 오른쪽 그림의 지구공전이라는 node를 구성하게 되면
지구Vworld = 지구Vlocal * 지구TM * 지구공전TM * 태양TM 이 변환식일것이다.

지구 공전 TM은 회전만하면 되기 때문에 회전행렬만 update가 되고 위치나 크기는 전부 태양TM을 따르게된다.

이렇게 되면 실질적으로 Render 할경우 태양은 자전(회전행렬)을 하지 않아도도 지구공전TM은 회전행렬을 update 하고 있으며 그 동시에 태양 TM의 위치와 크기를 가지고 있다. 이제 지구TM에 부모TM(지구공전TM * 태양TM )을 곱해주면 지구는 지구TM의 회전행렬은 지구공전의 회전행렬이 연산되고 태양 TM의 위치,크기값이 연산됨으로 의도한 결과값을 낼수 있다.

 

즉, 지구공전,달 공전은 회전행렬만 Update하는 객체(Obj)이며 위치나 크기는 부모들을 따라간다.

내용이 많이 길어지고 복잡해졌지만 적고싶었던 이유는 <계층구조는 무조건 Mesh로만 구성되어있는것이 아니다>는것을 말하고 싶었고, 원하는 의도를 낼려면 이런식으로도 사용 할 수 있다는 것을 적고싶었다.

 

<태양계 생성>

//DirectX11Graphics.h
class DirectX11Graphics 
{
private:
	...
    
    DXGameObject cameraObj; // 카메라 Obj
    
    // PrimitiveModel은 mesh data와 vertex shader에서 사용할 행렬 data가 담겨있다.
    // 즉, Textuer를 렌더링하는 data를 담당하는 변수 
	std::unique_ptr <PrimitiveModel> Earth; 

    //Component 구조로 설계한 DXGameObject를 사용
    //Transform으로 크기행렬,회전행렬,이동행렬 수정 및 Set할때 사용됨 
    //RenderComponent만 추가하여 Render 할때 사용되는 변수 
	DXGameObject EarthObj; 
  
    //====태양계 생성으로 인한 태양,달 추가 =====//
    std::unique_ptr <PrimitiveModel> Sun; 
    std::unique_ptr <PrimitiveModel> Moon; 
    
    DXGameObject SunObj; 
    DXGameObject MoonObj; 
    DXGameObject Sun_Earth_Obj; //지구 공전 obj
    DXGameObject Earth_Moon_Obj; //달 공전 obj
    
    ...
}

//DirectX11Graphics.cpp
void DirectX11Graphics::Update() // update 영역에서 지구를 자전시키는 코드를 생성
{
	...
    
    // timer class를 만들어서 델타타임을 활용 하게 만듬
	const float deltaTime = timer->GetMilisecondsElapesed();
	static float earthYaw = 0;
	
    earthYaw += deltaTime * 0.1f;
	EarthObj.GetTransform()->SetLocalRotation({ 0,earthYaw, 0 });  // 지구의 y축 회전(자전)
    
	//====태양계 생성으로 인한 태양,달 추가 =====//
	static float SunYaw =0 , MoonYaw =0;
	SunYaw += deltaTime * 0.05f;
	MoonYaw += deltaTime * 0.1f;
    
	SunObj.GetTransform()->SetLocalRotation({ 0, SunYaw, 0 }); // 태양의 y축 회전(자전)
	Sun_Earth_Obj.GetTransform()->SetLocalRotation({ 0, SunYaw, 0 }); // 태양-지구의 y축 회전(지구 공전)
	Earth_Moon_Obj.GetTransform()->SetLocalRotation({ 0, MoonYaw, 0 }); // 달의 y축 회전(자전)
	MoonObj.GetTransform()->SetLocalRotation({ 0, MoonYaw, 0 }); // 지구-달 의 y축 회전(달 공전)
    
    ...
}

//Scenc(장면) Init 함수 (화면에 보여지는 구성 요소들을 초기화하는 함수)
bool DirectX11Graphics::InitializeScene()
{
	...
    //====태양계 생성으로 인한 태양,달 추가 및 node연결 작업 =====//
    //태양
   	Sun = std::make_unique<PrimitiveSphere>(); //mesh를 구 모양으로 초기화 
    Sun->Set_size(10); //태양의 Scale Set
    Sun->Initialize(device.Get(), deviceContext.Get(), constantMatricesBuffer); //Sun Model Init
    Sun->MakePrimitive("Textures\\sun1024.jpg"); // mesh에 텍스쳐 이미지 추가
    SunObj.AddComponent<DXMeshRenderer>()->SetModel(parentPrimitive.get());//Renderer component를 추가후 Sun Mesh를 저장

    //지구공전(Sun_Earth_Obj)의 부모를 태양(SunObj)로 Set
    Sun_Earth_Obj.GetTransform()->SetParent(SunObj.GetTransform());
   
    //지구
    Earth = std::make_unique<PrimitiveSphere>(); //mesh를 구 모양으로 초기화 
    Earth->Set_size(3); //지구의 scale Set
    Earth->Initialize(device.Get(), deviceContext.Get(), constantMatricesBuffer);// Earth Model Init
    Earth->MakePrimitive("Textures\\earth2048.bmp"); // mesh에 텍스쳐 이미지 추가
    EarthObj.AddComponent<DXMeshRenderer>()->SetModel(Earth.get()); //Renderer component를 추가후 Earth Mesh를 저장
    
     // 지구의 x축 좌표 이동(지구 공전 obj 기준으로 x축이동)
    add_pos = { 25.0f,0,0 };
    EarthObj.GetTransform()->AdjustPosition(add_pos); 
    
    //지구(EarthObj)의 부모를 지구공전(Sun_Earth_Obj)로 Set
    EarthObj.GetTransform()->SetParent(Sun_Earth_node.GetTransform());

     //달 공전(Earth_Moon_Obj)의 부모를 지구(EarthObj)로 Set
    Earth_Moon_Obj.GetTransform()->SetParent(EarthObj.GetTransform());
	
	//달
    Moon = std::make_unique<PrimitiveSphere>(); // 구 모양 mesh
    Moon->Set_size(1);
    Moon->Initialize(device.Get(), deviceContext.Get(), constantMatricesBuffer);
    Moon>MakePrimitive("Textures\\moon1024.bmp"); // mesh에 텍스쳐 이미지 추가
    MoonObj.AddComponent<DXMeshRenderer>()->SetModel(childPrimitive.get());
    
    // 달의 x축 좌표 이동(달 공전 obj 기준으로 x축이동)
    add_pos = { 5.0f,0,0 };
    MoonObj.GetTransform()->AdjustPosition(add_pos);
    
     //달(MoonObj)의 부모를 지구공전(Earth_Moon_Obj)로 Set
    MoonObj.GetTransform()->SetParent(Earth_Moon_Obj.GetTransform());
    
    ...
}

void DirectX11Graphics::RenderFrame() 
{
	...
    //GameObject Primitive Mesh Draw
    //world space에 배치된 obj를 뷰투영행렬을 연산해 카메라시점으로 변환
    //인자값으로 카메라obj에 있는 뷰투영행렬을 Get
	EarthObj.Draw(cameraObj.GetComponent<DXCamera>()->GetViewProjectionMatrix());
    
     //====태양계 생성으로 인한 태양,달의 카메라시점 변환 =====//
   	SunObj.Draw(cameraObj.GetComponent<DXCamera>()->GetViewProjectionMatrix());
    MoonObj.Draw(cameraObj.GetComponent<DXCamera>()->GetViewProjectionMatrix());
    
    swapChain->Present(NULL, NULL);
}
최종적으로 이런 간단한 태양계가 렌더링된다.

 

unique함수는

https://velog.io/@whipbaek/c-unique-%ED%95%A8%EC%88%98%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC

 

c++ unique() 함수에 관하여

공부하게 된 배경https://programmers.co.kr/learn/courses/30/lessons/12906위 문제를 푸는데 unique와 erase의 조합 한 줄 만으로 해결하는 코드가 있었다. 문제를 풀 때는 생각이 안났으나 두 함수 모두 대강 알

velog.io

의 블로그를 참고하여 작성하였습니다.


unique(구간의 시작값, 구간의 마지막값) ;

-범위내에 인접하는(연속적인) 중복 요소들을 제거해준다.

-정렬 후에 배열의 사이즈가 바뀌지 않음

-배열의 사이즈가 바뀌지 않고 중복요소인 원소들은 뒤로 밀려난다.

-반환값은 중복없이 나열된 마지막 원소 다음의 반복자를 반환한다.

- <algorithm> 헤더를 사용한다.

 

unique함수 하나로는 vector 중복 원소 제거를 하는것은 어렵다.sort()함수를 사용하여 정렬 한 뒤 erase()함수를 활용하여 쓰레기값을 제거해줘야한다. 

 

 

#include<iostream>
#include<vector>
#include<algorithm>

void main()
{
    std::vector<int> v1 = { 10,10,20,30,20,10 };

    std::vector<int>::iterator iter = unique(v1.begin(), v1.end());

    for (int index : v1)
    {
        std::cout << index << " ";
    }
    std::cout << " 반환값 : " << *iter << " " << &iter << std::endl;

    //======================================================

    std::vector<int> v2 = { 10,10,20,30,20,10 };

    sort(v2.begin(), v2.end()); // 10,10,10,20,20,30
    iter = unique(v2.begin(), v2.end());
    for (int index : v2)
    {
        std::cout << index << " ";
    }
    std::cout << " 반환값 : " << *iter << " " << &iter << std::endl;
	
    v2.erase(iter, v2.end());
    // v2.erase(unique(v2.begin(), v2.end()), v2.end());
    //unique와 erase함수를 사용하여 정렬한 vector의 중복된 원소를 제거할 수 있다.  

    for (int index : v2)
    {
        std::cout << index << " ";
    }


    return;
}

 

<출력 결과>

첫번째 결과 : 정렬없이 unique함수를 동작했을때 반환되는 값은 v1[4]의 반복자가 반환된다.

두번째 결과 : 오름차순 정렬 뒤에 unique함수를 동작했을때 10의 원소가 한개씩 20,30으로 변경 되었다. 하지만 의미 중복을 제거하는 것이 목적이기에 의미 없는값들이다. (작동원리는 밑에서 서술)

세번쨰 결과 : v2.erase함수를 범위 : iter(v2[3]) ~ v2.end() 만큼 제거한뒤 출력한 값이다. 

 

<작동 원리>

더보기

위 링크는 첫번째 결과값을 작동원리를 설명했으니 나는 정렬한 두번째 값(10 10 10 20 20 30)으로 설명을 해보겠다.

//unique함수 원문
template <class ForwardIterator>
  ForwardIterator unique (ForwardIterator first, ForwardIterator last)
  {
  if (first==last) return last;

  ForwardIterator result = first;
  while (++first != last)
  {
    if (!(*result == *first))  // or: if (!pred(*result,*first)) for version (2)
      *(++result)=*first;
  }
  return ++result;
}//출처 : https://www.cplusplus.com/reference/algorithm/unique/

  if (first==last) return last;

first iter와 last iter가 같은경우는 원소가 1개 이하라는 뜻이므로 end() 값을 리턴

 

 

first, last , result iter(반복자) 초기화 완료
while 반복문의 함수 동작후 확인, // while문이 참이므로 반복분 시작

while 반복문에 들어와서 

 if (!(*result == *first)) , if조건문이 있는데  result와 first가 같으므로 true에 !를 만나 false를 반환

 

다시 while 반복문의 조건을 확인해서 계속 roop 시키면 ......

result와 first의 값이 달라지는 경우가 발생

 

 if (!(*result == *first)의 true임으로 if문이 동작한다.

*(++result)=*first; 동작 완료시

*first 가 가르키고 있는 20의 data를 result의 다음 반복자(++result)에 data를 정의하는것이다. 

그럼 vector의 [1] index의 data는 10-> 20으로 변경된다. 그리고 이 과정을 while이 false가 나올때까지 반복하게 되면  

중복 되지 않은 원소는 전부 앞으로 정렬하게 되고, 중복된 원소들은 뒤로 밀리게 되며 반환되는 return 값은 현재 가르키고있는 result의 다음 iter 값이 반환된다. 즉, unique함수의 return값은 쓰게기값(중복값)들이 모여있는 첫번째 인덱스를 iter를 반환하게 된다. 

 

이 점을 이용하여 unique한뒤 erase함수로 현재 반환 받은 값에서부터 end() 까지 지워버리면 

10 20 30 만 남게 되는것이다. 

 

reverse_iterator(역방향 반복자) : 반복자(iterator)와는 정반대로 동작하는 반복자

++,-- 연산자를 이용하여 iter를 변경하고 그랬는데 반대로 동작하게된다.

 

std::vector에서 자주 사용해온 begin(), end() 명령어는 반환값으로 iter(반복자)를 반환하는데

rbegin(), rend()는 riter(역방향반복자)를 반환한다.

즉,begin() = rend() , end() = rbegin() 인것이다.

 

예제코드)

#include <iostream>
#include <vector>
#include <algorithm>

int main(void)
{
    std::vector<int> v = { 10,20,30,40,50 };
	
    //벡터의 첫 원소부터 차례대로 출력
    for (std::vector<int>::iterator iter = v.begin(); iter != v.end(); iter++)
    {
        int num = *iter;
        cout << num;
    }

    cout <<endl;
    
    //벡터의 뒷 원소부터 역으로 출력 ( riter가 ++ 로 증가하지만 역으로 감소한다)
    for (std::vector<int>::reverse_iterator riter = v.rbegin(); riter != v.rend(); riter++)
    {
        int num = *riter;
        cout << num;
    }

}

//출력 결과
1020304050
5040302010

이 역방향 반복자를 이용하여 vector 자료형의 내림차순 정렬을 쉽게 만들 수 있다.

내림차순 정렬을 하는 방법은 많지만 주로 코딩테스트를 풀 때 이 방법들을 사용해 왔다.

(sort(구간의 시작값, 구간의 끝값) , sort 함수는 algorithm 헤더에 포함되어있다,)

 

1.sort함수에서 조건자에 grearter<자료형>() 을 쓴다.
2.조건자를 재량것 compare 함수를 만들어 쓴다.

3.sort함수로 오름차순 정렬한것을 reverse()로 뒤집어 내림차순 정렬로 만들어 쓴다.

4.sort함수의 인자값을 역방향반복자를 매개변수로 넣는다. (이 코드 만들수 있다.)

 

예제 코드)

#include <iostream>
#include <vector>
#include <algorithm>

void main()
{
	std::vector<int> v = { 1,5,2,8,4,9 };

	//오름차순 정렬
	sort(v.begin(), v.end());
	
	for (int index : v)
	{
		std::cout << index << " ";
	}
	std::cout << std::endl;

	//내림차순 정렬 
	sort(v.rbegin(), v.rend()); // 이부분이 v의 원소들을 내림차순 정렬을 한다. 
	for (int index : v)
	{
		std::cout << index << " ";
	}
	return;
}

//출력 결과
//1 2 4 5 8 9 (오름차순 정렬)
//9 8 5 4 2 1 (내림차순 정렬)

 

'C++' 카테고리의 다른 글

unique() (vector의 중복 값 제거)  (0) 2023.06.12
STL 범용 수치 알고리즘 (accumulate , inner_product)  (0) 2023.06.09
문자집합(Character Set)  (0) 2023.06.04

 

//accumulate 함수의 원문
#include <numeric>

template <class InputIterator, class T>							
T accumulate (InputIterator first, InputIterator last, T init);

template <class InputIterator, class T, class BinaryOperation>	
T accumulate (InputIterator first, InputIterator last, T init, BinaryOperation binary_op);

accumulate(구간의 시작 값, 구간의 마지막 값 , 초기 값)

-지정한 구간에 속한 값들을 모든 더한 값을 계산한다.

-기본적으로 더하기 연산, 조건자 이용시 이외의 연산 가능 (binary_op) 

-필요 헤더 : <numeric>

 

예제 코드)

#include <iostream>
#include <vector>
#include <numeric>

void main()
{
	std::vector<int> v1 = { 1,2,3,4 };

	int result = std::accumulate(v1.begin(), v1.end(), 0);

	std::cout << result;

	return;
}

//출력결과
//10
//1 + 2 + 3 + 4 = 10

//inner_product 함수 원문
#include <numeric>

template <class _InIt1, class _InIt2, class _Ty, class _BinOp1, class _BinOp2>
_Ty inner_product(_InIt1 _First1, _InIt1 _Last1, _InIt2 _First2, _Ty _Val, _BinOp1 _Reduce_op, _BinOp2 _Transform_op)

template <class _InIt1, class _InIt2, class _Ty>
_Ty inner_product(const _InIt1 _First1, const _InIt1 _Last1, const _InIt2 _First2, _Ty _Val)

inner_product는 내적이란 뜻이며 벡터(방향,크기를 가지고있는 값)를 마치 수처럼 곱하는 개념)을 뜻한다.

a라는 std::vecotr<int> b라는  std::vecotr<int> 있다고 가장할때

inner_product(시작값a,마지막값a,시작값b,초기값);

-두 입력값의 내적을 계산하는 알고리즘으로 기본적으로 +,*을 사용한다.

-반환값은 a,b의 값을 곱한뒤 서로 더한값이다.

-두번째(b)의 크기는 첫번째(a)의 크기보다 크거나 같아야한다.

 

p.s 벡터의 내적은 벡터의 곱(dot product) 라고도 부르며 벡터 사이의 크기(스칼라)를 나타날때 사용된다. 

(수학 - 벡터 관련 수학 작성후  링크 필요)

 

예제코드)

#include <iostream>
#include <vector>
#include <numeric>

void main()
{
	std::vector<int> v1 = { 1,2,3 };
	std::vector<int> v2 = { 4,5,6 };

	int result = std::inner_product(v1.begin(),v1.end(),v2.begin(),0);

	std::cout << result;

	return;
}

//출력결과
//32
//0(초기값) + (1 * 4) + (2 * 5) + (3 * 6) = 32

 

더보기 주의 , 코드에 관한 설명이 간략히 있지만 코드 내용 원문이 있으니 주의 할 것

 

<나의 풀이>

더보기

이 문제는 규칙에 의한 이진 변환시 roop 돌은 횟수roop를 돌면서 지운 0의 갯수를 구하는 문제이다.

 

<플로우 차트>

1.0 제거하기

1-1.문자열 s안에 있는 0과 1을 내림차순 정렬 하기

1-2. 문자열의 0의 첫번째 index값을 찾음

1-3. index값 반환에 성공한경우 if문 동작해서 0을 제거

2. 0을 제거한 문자열 s의 길이 체크후 이진변환

2-1. 문자열을 2로 계속 나눠서 이진 변환시키기

 

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> solution(string s) {
    vector<int> answer;
    int roop = 0; //roop를 돈 횟수 
    int zero_del = 0; //roop를 돌면서 지워진 0의 갯수
	
    //s이 "1"이되면 roop 종료 
    while (s != "1")
    {
        int zero_index = 0;

        //1. 0제거하기
        sort(s.rbegin(), s.rend());//내림차수 정렬 ex)111100000
        zero_index = s.find("0");
		
        //0을 찾을경우 if문 동작
        if (zero_index != -1)
        {
            zero_del += s.length() - zero_index; // 현재 roop에서 지운 0의 갯수 카운트
            s.erase(s.begin() + zero_index, s.end());
            //문자열의 0 첫번째 index(s.begin() + zero_index) , 문자열 끝index를 지워주면 1만 남게된다.
        }
       

        //2. 0을 제거한 s의 길이 체크후 이진변환
        int zero_del_str = s.length();
        s = ""; //s문자열 초기화 

        while (zero_del_str != 0)
        {
        //10진수를 2진수로 변환하는 방법
            s += to_string(zero_del_str % 2);
            zero_del_str /= 2;
        }
        reverse(s.begin(), s.end());// s문자열 원소를 뒤집어 원하는값으로 바꾸기 
        roop++; //1회 roop
    }


    return { roop,zero_del};
}

<다른사람의 풀이>

더보기

이진수를 vector<bool>값으로 나타냈으며 , 반복문 while에 들어가기전 for_each문을 사용하여 문자열s의 값을 vector<bool> 이진수 로 변경을 하였고 반복문에서 반복작업을 하였다. 

#include <string>
#include <vector>
#include <algorithm>
using namespace std;

vector<int> solution(string s)
{
    int zeros{ 0 }, num_transform{ 0 };
    vector<bool> bin;

    for_each(s.cbegin(), s.cend(), [&bin](const char c) {bin.emplace_back(c == '1'); });  //s를 이진수로 변환

    while (true)
    {
        if (bin == vector<bool>{true})
            break;
        
        int ones = count(bin.cbegin(), bin.cend(), true);    //1갯수를 셈
        zeros += bin.size() - ones;                          //0갯수를 셈
        bin.clear();
        
        while (ones > 0)
        { 
            bin.emplace_back(ones % 2);
            ones /= 2; 
        }//1갯수를 2진수로 바꿈. 순서는 거꾸로지만 계산에는 영향없음
        
        ++num_transform;                                   //이진변환 횟수 기록
    }

    return { num_transform,zeros };
}

https://school.programmers.co.kr/learn/courses/30/lessons/12906

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

더보기 주의 , 코드에 관한 설명이 간략히 있지만 코드 내용 원문이 있으니 주의 할 것

 

<나의 풀이>

더보기

현재 index와 다음 index 값을 비교해서 다를경우,

answer에 현재 index값을 저장 후 roop를 진행한다.

index+1 했을때 arr의 size보다 크거나 같을경우 마지막 숫자를 answer에 추가하면서

반복문을 탈출한다.

#include <vector>
#include <iostream>

using namespace std;

vector<int> solution(vector<int> arr) 
{
    vector<int> answer;
	
    //arr의 갯수만큼 roop
    for(int i = 0 ; i< arr.size() ; i++)
    {
    	//배열이 범위를 벗어나지 않게 하기위한 조건문
        if( i+1 >= arr.size())
        {
            answer.push_back(arr[i]);
            break;
        }
        
        //현재 index와 다음 index가 다를경우 값을 저장 
        if(arr[i] != arr[i+1])
        {
            answer.push_back(arr[i]);
        }
    }
    
    return answer;
}

<다른사람의 풀이>

더보기

algorithm헤더의 unique함수와 std::vector의 erase 함수 두개를 활용하면 해당 배열의 원소 중 중복값을 없앨수 있다.

 

unique 함수 : 배열에서 원소를  앞에서부터 채워나가며 채운 함수중에 중복이 있으면 뒤로 보내는 함수이다.

unique함수는 중복되지 않은 값들을 나열 한뒤 남은 중복 원소의 첫번째 iterator 값을 반환, 

unique함수를 사용하기전에 sort 함수를 하여 정렬할 필요가 있다.

 

(unique함수로부터 반환된 iterator , 배열의 끝  iterator) 범위를 eraser함수하면
함수 고유의 값들만 남아있다. 

 

하지만 이 문제는 함수의 순서를 바꾸지 말라고 하였기에 sort함수를 사용하지 않고 바로 unique함수를 활요한 것이다.

 

#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;

vector<int> solution(vector<int> arr) 
{
	//vector 에서 중복되는 함수들을 한곳에 모아 삭제할때 쓰는 구조다.
    arr.erase(unique(arr.begin(), arr.end()),arr.end());

    vector<int> answer = arr;
    return answer;
}

 

'코딩테스트 > 프로그래머스' 카테고리의 다른 글

LV2.숫자 변환하기  (0) 2023.10.26
LV2.2개 이하로 다른 비트  (0) 2023.10.26
LV.2 최솟값 만들기  (0) 2023.06.09
LV.1 내적  (0) 2023.06.09
LV.1 없는 숫자 더하기  (0) 2023.06.09

+ Recent posts