https://www.acmicpc.net/problem/1325

 

그래프를 입력받고 BFS를 통해 모든 노드에서 탐색을 시작하여 모든 노드를 순회하며 하나의 컴퓨터가 몇개의 컴퓨터와 신뢰적인 관계인지 횟수를 더해주면 된다. 그리고 최대 관계 수를 구한 다음 그에 맞는 노드를 출력해주면 된다.

 

 

정답코드

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

void BFS(int node);
vector<vector<int>> computers;
vector<bool> visited;
vector<int> answers;
int main()
{
	int n, m;

	cin >> n >> m;

	computers.resize(n + 1);
	answers.resize(n + 1);
	//그래프 입력
	for (int i = 0; i < m; i++)
	{
		int s, e;
		cin >> s >> e;
		computers[s].push_back(e);
	}

	visited.resize(n + 1);

	//BFS
	for (int i = 1; i <= n; i++)
	{
		fill(visited.begin(), visited.end(), false);
		BFS(i);
	}

	//최대값 찾기
	int max_val = 0;
	for (int i = 1; i <= n; i++)
	{
		max_val = max(max_val, answers[i]);
	}

	//검사
	for (int i = 1; i <= n; i++)
	{
		if (answers[i] == max_val)
		{
			cout << i << " ";
		}
	}
	

	return 0;
}

void BFS(int node)
{
	queue<int> q;
	q.push(node);
	visited[node] = true;

	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		for (int i : computers[cur])
		{
			if (visited[i] == false)
			{
				visited[i] = true;
				//몇개의 컴퓨터랑 관계되어 있는지
				answers[i]++;
				q.push(i);
			}
		}
	}
}

https://www.acmicpc.net/problem/18352

 

그래프를 입력받고 BFS를 통해 X부터 경로를 탐색하면서 각 도시 까지의 거리를 기록한다.

BFS를 통한 순회가 끝나면 x로 부터 k거리에 있는 도시를 오름차순으로 출력해주면 된다.

 

정답코드

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

using namespace std;

void BFS(int node);

vector<vector<int>> cities;
vector<int> visitied;
vector<int> answer;
int main()
{
	int n, m, k, x;

	cin >> n >> m >> k >> x;

	cities.resize(n + 1);

	//그래프 입력받기
	for (int i = 0; i < m; i++)
	{
		int s, e;
		cin >> s >> e;
		cities[s].push_back(e);
	}

	visitied.resize(n + 1);

	//초기값 세팅
	for (int i = 0; i <= n; i++)
	{
		visitied[i] = -1;
	}

	BFS(x);

	for (int i = 0; i <= n; i++)
	{
		if (visitied[i] == k) answer.push_back(i);
	}

	if (answer.empty()) cout << "-1" << "\n";
	else
	{
		sort(answer.begin(), answer.end());
		for (int tmp : answer)
		{
			cout << tmp << "\n";
		}
	}
}


void BFS(int node)
{
	queue<int> q;
	q.push(node);
	visitied[node]++;

	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		for (int i : cities[cur])
		{
			if (visitied[i] == -1)
			{
				visitied[i] = visitied[cur] + 1;
				q.push(i);
			}
		}
	}
}

 

오늘은 Compute Shader에 대해 복습하고 예제코드를 분석해보자.

 

Compute Shader에 대해 복습하기 전에 우선 CPU와 GPU의 차이에 대해 먼저 복습해보자

https://developer.nvidia.com/blog/cuda-refresher-reviewing-the-origins-of-gpu-computing/

 

CPU는 연산의 주체가 되는 ALU가 적은대신 메모리 비중이 높다. 

GPU는 연산의 주체가 되는 ALU가 많아서 단순한 연산에 강점을 가지고 코어가 많기 때문에 병렬로 처리하기에 적합하다.

Compute Shader는 이러한 GPU의 특징을 살려서 분할 정복과 같은 병렬성 알고리즘을 통해 단순하지마 연산양이 많은  데이터를 처리한다. 이를 통해 렌더링에는 관련없지만 연산이 필요한 경우에 Compute Shader를 사용할 수 있다.

Compute Shader는 Pipeline 상에는 없고 독립적으로 사용가능하다.

 

먼저 첫번째 예시 코드를 실행해보자. 실행해보면 result.txt 파일이 다음과 같이 생성된다. 

이는 데이터 2개를 던져주고 GPU를 통해 덧셈연산을 해준 것이다.

 

코드상에서 살펴보자면  밑의 두개의 함수가 이 연산에 관여하고 있다.

BuildBuffersAndViews();

DoComputeWork();

그리고 SRV두개를 통해 입력을 받아온 다음에 넘겨주고 UAV를 통해 결과값을 받아온다.

ComPtr<ID3D11Buffer> _outputBuffer;
ComPtr<ID3D11Buffer> _outputDebugBuffer;

ComPtr<ID3D11ShaderResourceView> _inputASRV;
ComPtr<ID3D11ShaderResourceView> _inputBSRV;
ComPtr<ID3D11UnorderedAccessView> _outputUAV;

그리고 해당 값을 받아오기 위해서 Shader쪽에서는 StructuredBuffer 변수를 통해 받아주고 덧셈 연산을 한 결과를 결과

StructuredBuffer에 저장해주고 있다.

struct Data
{
	float3 v1;
	float2 v2;
};

StructuredBuffer<Data> gInputA;
StructuredBuffer<Data> gInputB;
//READ + Write
RWStructuredBuffer<Data> gOutput;

[numthreads(32, 1, 1)]
void CS(int3 dtid : SV_DispatchThreadID)
{
	gOutput[dtid.x].v1 = gInputA[dtid.x].v1 + gInputB[dtid.x].v1;
	gOutput[dtid.x].v2 = gInputA[dtid.x].v2 + gInputB[dtid.x].v2;
}

 

여기서 CPU에서 GPU에 정보를 넘겨주기 위해 BuildBuffersAndViews() 함수를 통해 위에서 정의한 SRV와 UAV 버퍼의 옵션을 정해주고 초기화하여 만들어준다. 

void VecAddDemo::BuildBuffersAndViews()
{
	std::vector<Data> dataA(_numElements);
	std::vector<Data> dataB(_numElements);
	for (int i = 0; i < _numElements; ++i)
	{
		dataA[i].v1 = XMFLOAT3(i, i, i);
		dataA[i].v2 = XMFLOAT2(i, 0);

		dataB[i].v1 = XMFLOAT3(-i, i, 0.0f);
		dataB[i].v2 = XMFLOAT2(0, -i);
	}

	// Create a buffer to be bound as a shader input (D3D11_BIND_SHADER_RESOURCE).
	D3D11_BUFFER_DESC inputDesc;
	inputDesc.Usage = D3D11_USAGE_DEFAULT;
	inputDesc.ByteWidth = sizeof(Data) * _numElements;
	inputDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	inputDesc.CPUAccessFlags = 0;
	inputDesc.StructureByteStride = sizeof(Data);
	inputDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;

	D3D11_SUBRESOURCE_DATA vinitDataA;
	vinitDataA.pSysMem = &dataA[0];

	ComPtr<ID3D11Buffer> bufferA;
	HR(_device->CreateBuffer(&inputDesc, &vinitDataA, bufferA.GetAddressOf()));

	D3D11_SUBRESOURCE_DATA vinitDataB;
	vinitDataB.pSysMem = &dataB[0];

	ComPtr<ID3D11Buffer> bufferB;
	HR(_device->CreateBuffer(&inputDesc, &vinitDataB, bufferB.GetAddressOf()));

	// Create a read-write buffer the compute shader can write to (D3D11_BIND_UNORDERED_ACCESS).
	D3D11_BUFFER_DESC outputDesc;
	outputDesc.Usage = D3D11_USAGE_DEFAULT;
	outputDesc.ByteWidth = sizeof(Data) * _numElements;
	outputDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
	outputDesc.CPUAccessFlags = 0;
	outputDesc.StructureByteStride = sizeof(Data);
	outputDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;

	HR(_device->CreateBuffer(&outputDesc, 0, _outputBuffer.GetAddressOf()));

	// Create a system memory version of the buffer to read the results back from.
	outputDesc.Usage = D3D11_USAGE_STAGING;
	outputDesc.BindFlags = 0;
	outputDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
	HR(_device->CreateBuffer(&outputDesc, 0, _outputDebugBuffer.GetAddressOf()));


	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	srvDesc.Format = DXGI_FORMAT_UNKNOWN;
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
	srvDesc.BufferEx.FirstElement = 0;
	srvDesc.BufferEx.Flags = 0;
	srvDesc.BufferEx.NumElements = _numElements;

	_device->CreateShaderResourceView(bufferA.Get(), &srvDesc, _inputASRV.GetAddressOf());
	_device->CreateShaderResourceView(bufferB.Get(), &srvDesc, _inputBSRV.GetAddressOf());


	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
	uavDesc.Format = DXGI_FORMAT_UNKNOWN;
	uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
	uavDesc.Buffer.FirstElement = 0;
	uavDesc.Buffer.Flags = 0;
	uavDesc.Buffer.NumElements = _numElements;

	_device->CreateUnorderedAccessView(_outputBuffer.Get(), &uavDesc, _outputUAV.GetAddressOf());
}

 

그리고 실행할 때 값을 바인딩해주고 계산작업에 넘겨주는 것은 DoComputeWork함수에서 이루어진다.

void VecAddDemo::DoComputeWork()
{
	D3DX11_TECHNIQUE_DESC techDesc;

	//바인딩
	Effects::VecAddFX->SetInputA(_inputASRV);
	Effects::VecAddFX->SetInputB(_inputBSRV);
	Effects::VecAddFX->SetOutput(_outputUAV);

	Effects::VecAddFX->VecAddTech->GetDesc(&techDesc);
	for (UINT p = 0; p < techDesc.Passes; ++p)
	{
		ID3DX11EffectPass* pass = Effects::VecAddFX->VecAddTech->GetPassByIndex(p);
		pass->Apply(0, _deviceContext.Get());

		//스레드 그룹지정
		_deviceContext->Dispatch(1, 1, 1);
	}

	// Unbind the input textures from the CS for good housekeeping.
	ID3D11ShaderResourceView* nullSRV[1] = { 0 };
	_deviceContext->CSSetShaderResources(0, 1, nullSRV);

	// Unbind output from compute shader (we are going to use this output as an input in the next pass, 
	// and a resource cannot be both an output and input at the same time.
	ID3D11UnorderedAccessView* nullUAV[1] = { 0 };
	_deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, 0);

	// Disable compute shader.
	_deviceContext->CSSetShader(0, 0, 0);

	std::ofstream fout("results.txt");

	// Copy the output buffer to system memory.
	_deviceContext->CopyResource(_outputDebugBuffer.Get(), _outputBuffer.Get());

	// Map the data for reading.
	D3D11_MAPPED_SUBRESOURCE mappedData;
	_deviceContext->Map(_outputDebugBuffer.Get(), 0, D3D11_MAP_READ, 0, &mappedData);

	Data* dataView = reinterpret_cast<Data*>(mappedData.pData);

	for (int i = 0; i < _numElements; ++i)
	{
		fout << "(" << dataView[i].v1.x << ", " << dataView[i].v1.y << ", " << dataView[i].v1.z <<
			", " << dataView[i].v2.x << ", " << dataView[i].v2.y << ")" << std::endl;
	}

	_deviceContext->Unmap(_outputDebugBuffer.Get(), 0);

	fout.close();
}

 

받아오는 결과버퍼를 묘사하는 UAV를 통해 값을 가져와서 최종결과값은 outputDebugBuffer에 넣어주고 있는 모습이다.

 

이렇게 CPU가 아닌 GPU를 통해 연산작업을 해주는 것을 어떻게 사용할 수 있는지는 다음 예제코드를 보고 분석해보자.

우선 예제코드를 실행해보면 Blur효과가 들어가있는 것을 볼 수 가 있다.

 

예제코드에 gaussian blur 가 적용되어 있는데 이때 모든 픽셀의 값에 Blur 연산을 적용해야하는데 이 부분을 GPU에 넘겨줘서 해주게 된다. 그리고 지금 OM단계에서 렌더타켓이 아니라 텍스처에 렌더링을 해주고 있는 것을 볼 수 있다. 이렇게 해주면 텍스처를 수정하는 것으로 실제 보여지는 화면을 수정해줄 수 있다.

	ComPtr<ID3D11ShaderResourceView> _offscreenSRV;
	ComPtr<ID3D11UnorderedAccessView> _offscreenUAV;
	ComPtr<ID3D11RenderTargetView> _offscreenRTV;
	ID3D11RenderTargetView* renderTargets[1] = { _offscreenRTV.Get()};
	//렌더타켓을 텍스처로
	_deviceContext->OMSetRenderTargets(1, renderTargets, _depthStencilView.Get());
    
    //연산해주고 교체
_blur.BlurInPlace(_deviceContext, _offscreenSRV, _offscreenUAV, 4);

 

그리고 쉐이더 연산을 보면 가져온 텍스처를 수직또는 수평으로 나눠서 연산을 해주고 있다. 이때 GroupMemoryBarrierWithGroupSync(); 함수를 통해 이 코드의 위에까지의 작업이 모든 스레드에서 끝날 때까지 기다리게 한다.

//입력- 텍스처
Texture2D gInput;
RWTexture2D<float4> gOutput;

#define N 256
#define CacheSize (N + 2*gBlurRadius)
//공유하는 버퍼
groupshared float4 gCache[CacheSize];

[numthreads(N, 1, 1)]
void HorzBlurCS(int3 groupThreadID : SV_GroupThreadID,
	int3 dispatchThreadID : SV_DispatchThreadID)
{
	//
	// Fill local thread storage to reduce bandwidth.  To blur 
	// N pixels, we will need to load N + 2*BlurRadius pixels
	// due to the blur radius.
	//

	//텍스처를 공유하는 버퍼에 캐싱하여서 사용
	if (groupThreadID.x < gBlurRadius)
	{
		// Clamp out of bound samples that occur at image borders.
		int x = max(dispatchThreadID.x - gBlurRadius, 0);
		gCache[groupThreadID.x] = gInput[int2(x, dispatchThreadID.y)];
	}
	if (groupThreadID.x >= N - gBlurRadius)
	{
		// Clamp out of bound samples that occur at image borders.
		int x = min(dispatchThreadID.x + gBlurRadius, gInput.Length.x - 1);
		gCache[groupThreadID.x + 2 * gBlurRadius] = gInput[int2(x, dispatchThreadID.y)];
	}

	// Clamp out of bound samples that occur at image borders.
	gCache[groupThreadID.x + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];

	// Wait for all threads to finish.
	GroupMemoryBarrierWithGroupSync();

	//
	// Now blur each pixel.
	//

	float4 blurColor = float4(0, 0, 0, 0);

	[unroll]
	for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
	{
		int k = groupThreadID.x + gBlurRadius + i;

		blurColor += gWeights[i + gBlurRadius] * gCache[k];
	}

	gOutput[dispatchThreadID.xy] = blurColor;
}

[numthreads(1, N, 1)]
void VertBlurCS(int3 groupThreadID : SV_GroupThreadID,
	int3 dispatchThreadID : SV_DispatchThreadID)
{
	//
	// Fill local thread storage to reduce bandwidth.  To blur 
	// N pixels, we will need to load N + 2*BlurRadius pixels
	// due to the blur radius.
	//

	// This thread group runs N threads.  To get the extra 2*BlurRadius pixels, 
	// have 2*BlurRadius threads sample an extra pixel.
	if (groupThreadID.y < gBlurRadius)
	{
		// Clamp out of bound samples that occur at image borders.
		int y = max(dispatchThreadID.y - gBlurRadius, 0);
		gCache[groupThreadID.y] = gInput[int2(dispatchThreadID.x, y)];
	}
	if (groupThreadID.y >= N - gBlurRadius)
	{
		// Clamp out of bound samples that occur at image borders.
		int y = min(dispatchThreadID.y + gBlurRadius, gInput.Length.y - 1);
		gCache[groupThreadID.y + 2 * gBlurRadius] = gInput[int2(dispatchThreadID.x, y)];
	}

	// Clamp out of bound samples that occur at image borders.
	gCache[groupThreadID.y + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];


	// 위의 1차 작업이 끝날때 까지 기다리기
	GroupMemoryBarrierWithGroupSync();

	//
	// Now blur each pixel.
	//

	float4 blurColor = float4(0, 0, 0, 0);

	[unroll]
	for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
	{
		int k = groupThreadID.y + gBlurRadius + i;

		blurColor += gWeights[i + gBlurRadius] * gCache[k];
	}

	gOutput[dispatchThreadID.xy] = blurColor;
}

https://www.acmicpc.net/problem/9461

 

이 문제는 규칙을 찾아서 점화식으로 만들고 매번 n번째까지 계산을 하면 시간 초과가 뜨기 때문에 미리 100개를 계산해두고 필요한 부분만 꺼내 쓰게 구현하면 된다. 

이때 규칙은 그림을 보면 n-2번째하고 n-3번째 삼각형의 한변의 길이를 더하면 n번째 삼각형의 한변의 길이가 되는 것을 볼 수 있다. 이때 100번째 까지 가다보면 값이 커질 수 있기 때문에 long배열을 사용하자

 

정답코드

#include <iostream>

using namespace std;

int main()
{
    int t;

    cin >> t;

    long p[101] = { 0 };

    p[1] = p[2] = p[3] = 1;

    // 배열 미리 계산 
    for (int i = 4; i <= 100; i++) {
        p[i] = p[i - 2] + p[i - 3];
    }

    for (int i = 0; i < t; i++) {
        int n;
        cin >> n;
        cout << p[n] << endl;
    }

    return 0;
}

 

 

오늘은 빌보드에 대해 복습하고 예제 코드를 분석해보자

빌보드는 오브젝트가 항상 플레이어를 바라보도록 하는 것이다. 이전에 빌보드는 구현할 때 여러 오브젝트를 생성 할때

정점 4개를 두고 VS단계에서 고쳐주고 늘리면서 사용하게 했다. 이 방법보다 더 나은 방법이 있는데 바로 Geometry Shader를 사용하는 방법이다.

https://learn.microsoft.com/ko-kr/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

 

Pipeline 중간 아래쪽에 보면 Geometry Shader가 있다. 이 단계의 역할은 정점을 인위적으로 늘리고 줄일 수 있는 정점 

분리기이다. 이전에 4개를 복사하던 것에서 1개를 두고 GS단계에서 고쳐주고 늘려주면 되는 것이다. 

코드를 살펴보기 전에 예제를 먼저 실행해보자.

 

기존에 나무가 추가되어 있는 화면에서 나무가 추가되어있는 모습을 볼 수 있다. 이 나무 하나하나가 모두 빌보드인 것이다. 사각형에 2D를 그려서 이 사각혀잉 플레이어를 바라보게 만들어 준 것이다.

코드를 살펴보자

우선 정점을 만드는 부분을 살펴보자면 TreePointSprite라는 위치와 크기만을 가진 구조체로 값을 받아주고 있으며 

struct TreePointSprite
{
	XMFLOAT3 pos;
	XMFLOAT2 size;
};

 

이를 랜덤하게 배치하지만 언덕위에만 배치할 수 있도록 x y z 값을 설정해준다. 이 정점을 설정해준 개수 만큼 만들어주고 버퍼를 통해 쉐이더에 전달해준다. 정점하나당 나무 하나가 만들어 지는 것이다. 이 정점 하나를 GS단계에서 동적으로 여러 정점으로 

void TreeBillboardDemo::BuildTreeSpritesBuffer()
{
	Vertex::TreePointSprite v[TreeCount];

	for (UINT i = 0; i < TreeCount; ++i)
	{
		float x = MathHelper::RandF(-35.0f, 35.0f);
		float z = MathHelper::RandF(-35.0f, 35.0f);
		float y = GetHillHeight(x, z);

		// Move tree slightly above land height.
		y += 10.0f;

		v[i].pos = XMFLOAT3(x, y, z);
		v[i].size = XMFLOAT2(24.0f, 24.0f);
	}

	D3D11_BUFFER_DESC vbd;
	vbd.Usage = D3D11_USAGE_IMMUTABLE;
	vbd.ByteWidth = sizeof(Vertex::TreePointSprite) * TreeCount;
	vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vbd.CPUAccessFlags = 0;
	vbd.MiscFlags = 0;
	D3D11_SUBRESOURCE_DATA vinitData;
	vinitData.pSysMem = v;
	HR(_device->CreateBuffer(&vbd, &vinitData, _treeSpritesVB.GetAddressOf()));
}

 

 

코드를 보면 기존의 VS단계에서 PS단계로 바로 넘어가는 것이 아니라 GS단계를 이제 거치는 것을 볼 수 있다. GS단계에서 정점을 4개로 불려주고 이 값을 PS단계로 전달하게 된다.

struct GeoOut
{
	float4 PosH    : SV_POSITION;
    float3 PosW    : POSITION;
    float3 NormalW : NORMAL;
    float2 Tex     : TEXCOORD;
    uint   PrimID  : SV_PrimitiveID;
};

 // 최대 정점4개
[maxvertexcount(4)]
//정점 하나가 들어옴	도형의 넘버링	입력받은 값을 연산하고 저장해줄 곳
void GS(point VertexOut gin[1],  uint primID : SV_PrimitiveID, inout TriangleStream<GeoOut> triStream)
{	
	//
	// 플레이어를 바라보게 
	//

	float3 up = float3(0.0f, 1.0f, 0.0f);
	float3 look = gEyePosW - gin[0].CenterW;
	look.y = 0.0f; // y-axis aligned, so project to xz-plane
	look = normalize(look);
	float3 right = cross(up, look);

	//
	// 4개 정점 만들기
	//
	float halfWidth  = 0.5f*gin[0].SizeW.x;
	float halfHeight = 0.5f*gin[0].SizeW.y;
	
	float4 v[4];
	v[0] = float4(gin[0].CenterW + halfWidth*right - halfHeight*up, 1.0f);
	v[1] = float4(gin[0].CenterW + halfWidth*right + halfHeight*up, 1.0f);
	v[2] = float4(gin[0].CenterW - halfWidth*right - halfHeight*up, 1.0f);
	v[3] = float4(gin[0].CenterW - halfWidth*right + halfHeight*up, 1.0f);

	//
	// 만든 정점을 결과값 저장하는 곳에 밀어넣기
	//
	GeoOut gout;
	[unroll]
	for(int i = 0; i < 4; ++i)
	{
		gout.PosH     = mul(v[i], gViewProj);
		gout.PosW     = v[i].xyz;
		gout.NormalW  = look;
		gout.Tex      = gTexC[i];
		gout.PrimID   = primID;
		
		triStream.Append(gout);
	}
}

 

굳이 정점을 이렇게 연산해서 넘겨주는 이유는 4개를 넘겨주는 것보다는 1개를 가지고 연산하는 것이 비용을 아껴줄 수 있기 때문이다.



오늘은 스텐실을 구현해둔 클래스를 살펴보자 

우선 스텐실이라고 했을 때 Depth Stencil을 먼저 떠올리게 된다. Depth Stencil에서 Depth는 깊이를 표현하는 것으로 물체의 깊이에 따라서 그려줄지 말지를 결정하는 것이고 Stencil은 픽셀 단이로 정보를 저장하며 렌더링 할 영역을 제어할 수 있는데 말 그대로 그려줄 수 있는 영역을 정해주게 된다. 특정 조건에 따라 픽셀의 렌더링을 허용하거나 차단할 수 있으며, 복잡한 이미지 효과나 마스킹, 포털 등의 특수 효과 구현에 사용한다.

 

이 두 정보는 텍스처를 통해 같이 전달해주게 되는데 텍스처desc에서 옵션 값중에 Format으로 값의 크기를 정해주는데 보통 다음과 같이 설정해준다.

desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

 

이렇게 해주면 깊이표현에 24비트 사용하고 스텐실에 8비트를 사용한다는 것이다. 24비트면 3바이트이다. float가 4바이트를 사용하는 것을 생각해보면 오차가 생길 수 있지만 깊이는 0~1까지 사용할 것이고 이 안에서 우리가 float를 활용하여 사용하기 때문에 24비트만 사용하는 것을 볼 수 있다.

스텐실은 0~ 255의 값을 사용할 수 있다.

 

이렇게 세팅해준 값을 우리가 활용해서 Depth Stencil로 한번에 제어해주면 되는 것이다.

DX에서는 ClearDepthStencilView를 통해 값을 Clear해주고 값을 세팅해주면 된다.

_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

 

이제 스텐실을 어떻게 사용하고 이 것으로 무엇을 할 수 있는지 살펴보기 위해 우선 예제코드를 실행해보자.

 

스텐실 관점에서 보자면 거울에 비친 모습이 해골을 하나 더 그린다고 생각하자면 어느 관점에서는 절반만 보이고 어느시점에서는 아예 안보일 때도 있다. 거울 영역에서만 잘라서 보여주게 되는 것이다. 이때 스텐실을 사용하게 된다.

이때 거울을 먼저 그리고 그 거울에 해당하는 부분에 스텐실을 뚫어주고 이 부분에서만 값을 갱신하게 해준다. 그리고 반사된 해골을 그려줄 때 스텐실이 1인 부분만 그려주게 해서 안보이는 나머지부분은 잘라주게 된다.

그림자에서도 그림자를 그리는 과정에서 같은 곳에 두번 연속해서 그림자를 그려준다고 하면 점점 진하게 보일텐데 그렇게 안되게 하려고 한번만 그려주게 하는 곳에 스텐실을 사용할 수 있다.

 

예제 코드에서 어떤식으로 스텐실을 사용하는 지 살펴보기 전에 RenderState부분에서 DepthStencil을 사용하기 위한 변수를 보자.

static ComPtr<ID3D11DepthStencilState> MarkMirrorDSS;
static ComPtr<ID3D11DepthStencilState> DrawReflectionDSS;
static ComPtr<ID3D11DepthStencilState> NoDoubleBlendDSS;

 

이렇게 값을 선언해주고 이 변수를 활용해서 값을 묘사해주고 넘겨주면 된다.

//
// MarkMirrorDSS
//

D3D11_DEPTH_STENCIL_DESC mirrorDesc;
mirrorDesc.DepthEnable = true;
mirrorDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
mirrorDesc.DepthFunc = D3D11_COMPARISON_LESS;			//DEPTH
mirrorDesc.StencilEnable = true;						//STENCIL
mirrorDesc.StencilReadMask = 0xff;						//255: 전부 1로 
mirrorDesc.StencilWriteMask = 0xff;						//255: 전부 1로

mirrorDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;			//각 상황에 맞는 규칙
mirrorDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;				//스텐실 무조건 통과

// We are not rendering backfacing polygons, so these settings do not matter.
mirrorDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

HR(device->CreateDepthStencilState(&mirrorDesc, &MarkMirrorDSS));

 

이렇게 만들어주고 렌더링 파이프라인에 연결 시켜준다음 우리가 사용하겠다라고 선언해줘야한다.  

이때 예제코드를 보면 우선 거울부분에 스텐실 버퍼에서만 그려주는 부분에 OMSetDepthStencilState 함수로 이를 설정해주고 있다.

//
// 스텐실 버퍼에 거울 그려주기
//

activeTech->GetDesc(&techDesc);
for (uint32 p = 0; p < techDesc.Passes; ++p)
{
	ID3DX11EffectPass* pass = activeTech->GetPassByIndex(p);

	_deviceContext->IASetVertexBuffers(0, 1, _roomVB.GetAddressOf(), &stride, &offset);

	// Set per object constants.
	XMMATRIX world = ::XMLoadFloat4x4(&_roomWorld);
	XMMATRIX worldInvTranspose = MathHelper::InverseTranspose(world);
	XMMATRIX worldViewProj = world * view * proj;

	Effects::BasicFX->SetWorld(world);
	Effects::BasicFX->SetWorldInvTranspose(worldInvTranspose);
	Effects::BasicFX->SetWorldViewProj(worldViewProj);
	Effects::BasicFX->SetTexTransform(XMMatrixIdentity());

	// 렌더타켓에 그려주지 않는다.
	_deviceContext->OMSetBlendState(RenderStates::NoRenderTargetWritesBS.Get(), blendFactor, 0xffffffff);

	//스텐실 사용- 스텐실 구멍 뚫어주기 - 1로 바꿔줌
	_deviceContext->OMSetDepthStencilState(RenderStates::MarkMirrorDSS.Get(), 1);

	pass->Apply(0, _deviceContext.Get());
	_deviceContext->Draw(6, 24);

	// 파이프라인 동작 방식 리셋
	_deviceContext->OMSetDepthStencilState(0, 0);
	_deviceContext->OMSetBlendState(0, blendFactor, 0xffffffff);
}

 

이렇게 해주면 거울부분에 스텐실 값이 모두 1이 된다. 이 다음에 스텐실에 반사된 해골을 그려주면 된다. 이때

RenderStates::DrawReflectionDSS.Get() 이 것을 설정해주면 스텐실 값이 1이랑 같을 때만 통과시켜주는 것으로 1인 부분만 그려주게 된다. 

//
// 반사된 해골 그려주기
//
activeSkullTech->GetDesc(&techDesc);
for (uint32 p = 0; p < techDesc.Passes; ++p)
{
	ComPtr<ID3DX11EffectPass> pass = activeSkullTech->GetPassByIndex(p);

	_deviceContext->IASetVertexBuffers(0, 1, _skullVB.GetAddressOf(), &stride, &offset);
	_deviceContext->IASetIndexBuffer(_skullIB.Get(), DXGI_FORMAT_R32_UINT, 0);

	XMVECTOR mirrorPlane = ::XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
	XMMATRIX R = ::XMMatrixReflect(mirrorPlane);
	XMMATRIX world = ::XMLoadFloat4x4(&_skullWorld) * R;
	XMMATRIX worldInvTranspose = MathHelper::InverseTranspose(world);
	XMMATRIX worldViewProj = world * view * proj;

	Effects::BasicFX->SetWorld(world);
	Effects::BasicFX->SetWorldInvTranspose(worldInvTranspose);
	Effects::BasicFX->SetWorldViewProj(worldViewProj);
	Effects::BasicFX->SetMaterial(_skullMat);

	// Cache the old light directions, and reflect the light directions.
	XMFLOAT3 oldLightDirections[3];
	for (int i = 0; i < 3; ++i)
	{
		oldLightDirections[i] = _dirLights[i].Direction;

		XMVECTOR lightDir = XMLoadFloat3(&_dirLights[i].Direction);
		XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir, R);
		XMStoreFloat3(&_dirLights[i].Direction, reflectedLightDir);
	}

	Effects::BasicFX->SetDirLights(_dirLights);

	// Cull clockwise triangles for reflection.
	_deviceContext->RSSetState(RenderStates::CullClockwiseRS.Get());

	// Only draw reflection into visible mirror pixels as marked by the stencil buffer. 
	_deviceContext->OMSetDepthStencilState(RenderStates::DrawReflectionDSS.Get(), 1);
	pass->Apply(0, _deviceContext.Get());
	_deviceContext->DrawIndexed(_skullIndexCount, 0, 0);

	// Restore default states.
	_deviceContext->RSSetState(0);
	_deviceContext->OMSetDepthStencilState(0, 0);

	// Restore light directions.
	for (int i = 0; i < 3; ++i)
	{
		_dirLights[i].Direction = oldLightDirections[i];
	}

	Effects::BasicFX->SetDirLights(_dirLights);
}

 

그 다음 최종적으로 거울을 그리고 해골이랑 섞기 위해 TransparentBS를 사용해준다.

	//
	//	 거울 그려주기 - 해골이랑 잘 섞일 수 있도록 블렌딩 조정
	// 

	activeTech->GetDesc(&techDesc);
	for (uint32 p = 0; p < techDesc.Passes; ++p)
	{
		ID3DX11EffectPass* pass = activeTech->GetPassByIndex(p);

		_deviceContext->IASetVertexBuffers(0, 1, _roomVB.GetAddressOf(), &stride, &offset);

		// Set per object constants.
		XMMATRIX world = ::XMLoadFloat4x4(&_roomWorld);
		XMMATRIX worldInvTranspose = MathHelper::InverseTranspose(world);
		XMMATRIX worldViewProj = world * view * proj;

		Effects::BasicFX->SetWorld(world);
		Effects::BasicFX->SetWorldInvTranspose(worldInvTranspose);
		Effects::BasicFX->SetWorldViewProj(worldViewProj);
		Effects::BasicFX->SetTexTransform(XMMatrixIdentity());
		Effects::BasicFX->SetMaterial(_mirrorMat);
		Effects::BasicFX->SetDiffuseMap(_mirrorDiffuseMapSRV.Get());

		// Mirror
		_deviceContext->OMSetBlendState(RenderStates::TransparentBS.Get(), blendFactor, 0xffffffff);
		pass->Apply(0, _deviceContext.Get());
		_deviceContext->Draw(6, 24);
	}

 

그리고 그림자를 그려줄 때 스텐실이 활용된 부분을 보면 NoDoubleBlendDSS를 통해 2번 그려지는 것을 방지한다. 이때 한번 그려졌다면 스텐실 값을 증가시키고  이렇게 증가시키면 전의 값과 같지 않기 때문에 2번 그려지는 것을 방지 할 수 있는 것이다.

_deviceContext->OMSetDepthStencilState(RenderStates::NoDoubleBlendDSS.Get(), 0);

https://www.acmicpc.net/problem/1904

 

 

시간제한이 0.75초 라서 점화식을 찾고 이를 동적 계획법으로 풀어서 시간 복잡도를 줄여야한다. 

일단 쓸 수 있는 타일은 00 또는 1 이다.

1번째일때, 1개

2번째일때, 2개이다. 

이후를 생각해보자면 

1을 붙이는 경우는  n-1길이의 숫자 뒤에 붙이면 된다.

00을 붙이는 경우는 n-2길이의 숫자 뒤에 붙이면 된다. 

이를 통해 점화식 dp[n] = dp[n-1] + dp[n-2] 

즉 n-1에 1을 붙이는 경우와 n-2에 00을 붙이는 경우를 더 해주면 되는 것이다.

 

정답코드

#include <iostream>

using namespace std;

//dp활용 : 점화식을 구해야한다.
int main()
{
	int n;

	cin >> n;

	if (n == 1)
	{
		cout << 1 << endl;
		return 0;
	}

	int p1 = 1;			//p[n-2]
	int p2 = 2;			//p[n-1]
	int p_n = 0;

	//dp계산
	for (int i = 3; i <= n; i++)
	{
		p_n = (p1 + p2) % 15746;
		p1 = p2;
		p2 = p_n;
	}

	cout << p2 << endl;

	return 0;
}

 

오늘은 블렌딩 클래스의 코드를 분석해보자.

우선 실행을 해보면 키보드에서 1,2,3을 누르는 것에 따라 기본 색만 적용된 버전과 텍스처가 적용된 버전, 안개가 있는 버전이 출력되는 것을 볼 수 있다.

 

여기서 블렌딩은 안개가 적용되어서 흐릿하게 보이는 것에 적용되어 있다. 만약 물을 반투명하게 만든다면 블렌딩이 적용되어야 할 것이다.

블렌딩은 말 그대로 색을 섞는 것이다. 이 블렌딩은 OM단계에서 연산이 된다. 

OM단계에서는 텍스처가 적용되어 있는 단계로 이 텍스처를 덮어쓸 지 아니면 색을 섞어 줄 지 계산 식을 통해 정해주면 된다.

텍스처클래스때와 달라진 점을 중점으로 살펴보자

우선 Init부분을 보면 

Effects::InitAll(_device, L"../Shaders/10. Basic.fx");
InputLayouts::InitAll(_device);
RenderStates::InitAll(_device);

Effects를 통해 shader파일을 불러오고 InputLayouts 클래스를 통해 정점이 어떻게 생겼는지에 대한 정보를 넘겨주고  있다. 

void InputLayouts::InitAll(ComPtr<ID3D11Device> device)
{
	D3DX11_PASS_DESC passDesc;

	//
	// Basic32
	//

	Effects::BasicFX->Light1Tech->GetPassByIndex(0)->GetDesc(&passDesc);
	HR(device->CreateInputLayout(InputLayoutDesc::Basic32, 3, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, &Basic32));

	//
	// TreePointSprite
	//

	Effects::TreeSpriteFX->Light3Tech->GetPassByIndex(0)->GetDesc(&passDesc);
	HR(device->CreateInputLayout(InputLayoutDesc::TreePointSprite, 2, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, &TreePointSprite));
}

 

또한 지금 블렌딩은 리소스를 사용해주는 단계이기 때문에 그려주는 단계에서 InputLayout쪽을 설정해주는 것을 볼 수 있다.

void BlendDemo::DrawScene()
{
	_deviceContext->IASetInputLayout(InputLayouts::Basic32.Get());
}

 

그리고 RenderStates 부분을 보면 BlendState 변수가 나오는데 여기서 블렌딩상태를 묘사하는 리소스를 만들어주고 이를 바탕으로 연산을 해주게 된다.

class RenderStates
{
public:
    static ComPtr<ID3D11BlendState> AlphaToCoverageBS;
    static ComPtr<ID3D11BlendState> TransparentBS;
    static ComPtr<ID3D11BlendState> NoRenderTargetWritesBS;
    
};

 

결국 핵심은 이 BlendState를 어떻게 묘사하고 사용하는 지이다. 

void RenderStates::InitAll(ComPtr<ID3D11Device> device)
{
    //
    // TransparentBS
    //

    D3D11_BLEND_DESC transparentDesc = { 0 };
    transparentDesc.AlphaToCoverageEnable = false;
    transparentDesc.IndependentBlendEnable = false;

    transparentDesc.RenderTarget[0].BlendEnable = true;		//블렌딩 사용
    transparentDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;		// src :a
    transparentDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;		//dst : 1-a
    transparentDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;				//계산 : +
    transparentDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;			//가중치src *1
    transparentDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;			//가중치dst *2
    transparentDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
    transparentDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;

    HR(device->CreateBlendState(&transparentDesc, &TransparentBS));
}

 

코드에서 만약 저렇게 사용한다고 하면 각 옵션의 사용법은 주석의 내용과 같다.

이렇게 묘사하는 것을 만들어주고 연결해주는 부분이 필요한데 이 부분은 OM단계의 코드를 보면 알 수 있다.

void BlendDemo::DrawScene()
{
	_deviceContext->OMSetBlendState(RenderStates::TransparentBS.Get(), blendFactor, 0xffffffff);
}

 

그리고 쉐이더에서 안개를 구현하는 쪽 코드를 살펴보자면 아래와 같다.

if (gFogEnabled)
{
    float fogLerp = saturate((distToEye - gFogStart) / gFogRange);

    // Blend the fog color and the lit color.
    litColor = lerp(litColor, gFogColor, fogLerp);
}

 

식을 보면 카메라에서 물체까지의 거리를 판별하고 안개의 시작점통해 연산을 해주는 것으로 안개가 어디서 부터 시작하고 최소 범위와 최대 범위를 통해 연산해주면 0~1사이의 값, 비율로 나오게 될 것이다. 이를 통해 litColor 즉 최종적인 색상에 안개의 색상을 섞에서 반환해주면 된다.

오늘은 텍스처 관련 데모앱을 분석해보자.

 

우선 데모앱을 실행해보면 텍스처가 적용된 큐브가 보이는데 잘보면 밑부분은 어둡게 되어있어 빛까지 적용된 것을 볼 수 있다. 

이 텍스처와 빛이 코드로 어떻게 구현되어있는지 살펴보자. 기존에는 uv좌표를 통해 텍스처를 매핑해줬었다. 
이때 uv좌표는 텍스처의 비율이라고 보면 된다. 만약 텍스처를 매핑했을 때 잘 떨어지지 않는 부분이 있다면 보간이 이루어지게 된다.

이번 코드에도 보면 SRV를 기반으로 텍스처를 묘사하고 이를 shader쪽에 넘겨줘서 uv좌표를 통해 맵핑을 해주는 것이다.

ComPtr<ID3D11ShaderResourceView> _diffuseMapSRV;

HRESULT hr = ::LoadFromWICFile(L"../Resources/Textures/WoodCrate01.dds", WIC_FLAGS_NONE, &md, img);
CHECK(hr);

// 여러 버전이 있음. (~CreateShaderResourceViewEx)
hr = ::CreateShaderResourceView(_device.Get(), img.GetImages(), img.GetImageCount(), md, _diffuseMapSRV.GetAddressOf());
CHECK(hr);

 

shader 코드에서 TEXCOORD부분이 uv좌표를 나타내는 부분이다. 이 uv좌표와 샘플러 함수를 통해 텍스처를 입혀주는 것이다.

struct VertexIn
{
	float3 PosL    : POSITION;
	float3 NormalL : NORMAL;
	float2 Tex     : TEXCOORD;
};

struct VertexOut
{
	float4 PosH    : SV_POSITION;
	float3 PosW    : POSITION;
	float3 NormalW : NORMAL;
	float2 Tex     : TEXCOORD;
};

float4 texColor = float4(1, 1, 1, 1);
if (gUseTexure)
{
	// Sample texture.
	texColor = gDiffuseMap.Sample(samAnisotropic, pin.Tex);
}

 

좀 더 자세히 알아보자

우선 큐브를 만드는 부분에서 uv좌표를 설정해줘야한다.

geoGen.CreateBox(1.0f, 1.0f, 1.0f, box);

 

이 CreateBox함수가 선언된 클래스의 헤더파일 코드를 보면 정점을 정의하는 구조체에서 UV좌표부분이 추가되어 있다. 

struct Vertex
{
	Vertex() : position(0, 0, 0), normal(0, 0, 0), tangentU(0, 0, 0), texC(0, 0) {}
	Vertex(const XMFLOAT3& p, const XMFLOAT3& n, const XMFLOAT3& t, const XMFLOAT2& uv)
		: position(p), normal(n), tangentU(t), texC(uv){}
	Vertex(
		float px, float py, float pz, 
		float nx, float ny, float nz,
		float tx, float ty, float tz,
		float u, float v)
		: position(px,py,pz), normal(nx,ny,nz),
		  tangentU(tx, ty, tz), texC(u,v){}

	XMFLOAT3 position;
	XMFLOAT3 normal;
	XMFLOAT3 tangentU;
	XMFLOAT2 texC;			//UV
};

 

그리고 텍스처를 SRV로 받아서 샘플링할 때도 여러가지 방법이 있는데 이것은 쉐이더 코드의 SamplerState를 통해 정의해줄 수 있다.

SamplerState samAnisotropic
{
	Filter = ANISOTROPIC;
	MaxAnisotropy = 4;

	AddressU = WRAP;		//넘어가면 반복
	AddressV = WRAP;
};

 

이때 조명은 여러 가지 계산을 해보면서 제일 자연스러운 것을 채택하는 것이 좋다. 여기서는 텍스처컬러에 Ambient와 Diffuse값을 더한 부분을 곱해주고 Specular는 이 값에 더해주어서 반짝이는 효과가 텍스처자체 색상에는 영향을 주지 않도록 했다.

// Modulate with late add.
litColor = texColor * (ambient + diffuse) + spec;

 

여기서 만약 파도와 같이 텍스처가 움직이는 모션에 맞춰주려면 uv좌표가 고정된 것이 아닌 변화해야한다. 

v[i].tex.x = 0.5f + _waves[i].x / _waves.Width();
v[i].tex.y = 0.5f - _waves[i].z / _waves.Depth();

 



이번엔 조명 코드를 분석해보자

우선 LightDemo를 실행시켜보면 실시간으로 Spot, Direct, Point Light가 적용돼서 화면이 보이는 것을 볼 수 있다.

또한 전에 배웠던 Diffuse Ambient Specular Light도 모두 적용되어 있는 모습을 볼 수 있다. 

이렇게 다양한 조명을 적용하는 것도 좋지만 조명이 많아질 수록 연산량이 늘어나기 때문에 필요한 조명만 필요한 영역에 사용하는 것이 좋다.

우선 LightDemo 코드를 보면 헤더파일에 LightHelper가 추가되어있는데 이는 각 조명에 대한 구조체 정의가 있는 부분이다. 

우선 DirectionalLight는 일직선으로 가는 조명으로 Ambient Diffuse Specular 3가지 특성을 가지고 있다.

struct DirectionalLight
{
	DirectionalLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;
	XMFLOAT3 Direction;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

PointLight는 어떤 Point로부터 모든 방향으로 동일한 세기의 빛을 뿜는 광원을 말한. 이 조명에는 Ambient,Diffuse,Specular 특성이 있고 Point의 위치와 영향을 줄 거리, 높이 등으로 이루어져있다. 

struct PointLight
{
	PointLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;

	// Packed into 4D vector: (Position, Range)
	XMFLOAT3 Position;
	float Range;

	// Packed into 4D vector: (A0, A1, A2, Pad)
	XMFLOAT3 Att;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

SpotLight는 손전등과 무대 조명과 같이 원뿔 모양으로 한곳을 비추는 조명으로 구조체 내부는 Point Light와 같다.

struct SpotLight
{
	SpotLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;

	// Packed into 4D vector: (Position, Range)
	XMFLOAT3 Position;
	float Range;

	// Packed into 4D vector: (Direction, Spot)
	XMFLOAT3 Direction;
	float Spot;

	// Packed into 4D vector: (Att, Pad)
	XMFLOAT3 Att;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

여기서 조명마다 있는 pad는 16바이트 정렬을 해주기 위한 값이다.

이렇게 정의해준 조명 중에 어떤 조명을 사용할 지 골라준 다음에 쉐이더에서 빛에 따른 색상 연산을 해주어야한다. 쉐이더 코드를 살펴보자면 우선 위의 조명 구조체와 구조를 맞춰준 구조체를 쉐이더쪽에서도 만들어줘야한다.

struct DirectionalLight
{
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;
	float3 Direction;
	float pad;
};

struct PointLight
{ 
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;

	float3 Position;
	float Range;

	float3 Att;
	float pad;
};

struct SpotLight
{
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;

	float3 Position;
	float Range;

	float3 Direction;
	float Spot;

	float3 Att;
	float pad;
};

조명에 대한 값을 매 프레임 받아주기 위해 상수버퍼를 추가해준 것을 볼 수 있다. 

cbuffer cbPerFrame
{
	DirectionalLight gDirLight;
	PointLight gPointLight;
	SpotLight gSpotLight;
	float3 gEyePosW;
};

 

그리고 쉐이더에서 각 조명을 연산해주는 코드를 살펴볼 것인데 대표적으로 DirectionalLight 코드만 분석해보자.

oid ComputeDirectionalLight(Material mat, DirectionalLight L, 
                             float3 normal, float3 toEye,
					         out float4 ambient,
						     out float4 diffuse,
						     out float4 spec)
{
	// Initialize outputs.
	ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	spec    = float4(0.0f, 0.0f, 0.0f, 0.0f);

	// The light vector aims opposite the direction the light rays travel.
	float3 lightVec = -L.Direction;

	// Add ambient term.
	ambient = mat.Ambient * L.Ambient;	

	// Add diffuse and specular term, provided the surface is in 
	// the line of site of the light.
	
	float diffuseFactor = dot(lightVec, normal);

	// Flatten to avoid dynamic branching.
	[flatten]
	if( diffuseFactor > 0.0f )
	{
		float3 v         = reflect(-lightVec, normal);
		float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);
					
		diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
		spec    = specFactor * mat.Specular * L.Specular;
	}
}

 

코드를 보면 조명의 특성 3가지를 동시에 연산한 다음에 최종적인 색상을 반환해주고 있다. 뱉어준 색상을 통해 PS단계에서 다른 조명의 색상이 있다면 이것까지 연산에 포함해서 최종적으로 화면에 보이는 색을 반환해준다. 

float4 PS(VertexOut pin) : SV_Target
{
	// Interpolating normal can unnormalize it, so normalize it.
	pin.NormalW = normalize(pin.NormalW);

	float3 toEyeW = normalize(gEyePosW - pin.PosW);

	// Start with a sum of zero. 
	float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);

	// Sum the light contribution from each light source.
	float4 A, D, S;

	ComputeDirectionalLight(gMaterial, gDirLight, pin.NormalW, toEyeW, A, D, S);
	ambient += A;
	diffuse += D;
	spec += S;

	ComputePointLight(gMaterial, gPointLight, pin.PosW, pin.NormalW, toEyeW, A, D, S);
	ambient += A;
	diffuse += D;
	spec += S;

	ComputeSpotLight(gMaterial, gSpotLight, pin.PosW, pin.NormalW, toEyeW, A, D, S);
	ambient += A;
	diffuse += D;
	spec += S;

	float4 litColor = ambient + diffuse + spec;

	// Common to take alpha from diffuse material.
	litColor.a = gMaterial.Diffuse.a;

	return litColor;
}

 

그리고 조명 코드 밑에 보면 Material이라는 구조체가 있는데 이는 물체의 특징으로 어떤 재질도 되어 있는지에 관한 정이다. 이 재질에 따라 빛을 반사하는 정도가 다를 것이다. 결국은 재질 과 빛의 특징 연산을 통해 최종적인 빛 연산이 이루어지게 된다.

 

연산의 이론적인 부분을 한번 복습 해보자.

우선 Ambient(주변광)는 빛의 반사를 생각해서 최소한의 빛을 보장하기 위해 빛의 색상을 일정 수준이라도 더 해준다. 

코드에서 보면 단순하게 Ambient값을 곱해주는 것을 볼 수 있다.

ambient = mat.Ambient * L.Ambient;

 

Diffuse(확산광)는 물체에 빛을 쏜다고 했을 때 물체 포면에 닿아서 여러 방향으로 확산되는 특징을 가지고 있으며 표면과 물체의 노멀 벡터 각도가 0도일 때 가장 강하며 90도일 때 가장 약하다. 이 빛은 물체의 재질에 영향을 많이 받는다.

이 값은 -1~ 1까지 의 범위를 가지고 있다. 계산은 -L과 N의 내적을 구해주면 cos값을 구할 수 있는데

이를 물체 재질값과 Diffuse 값을 곱해주면 된다.

코드 상에서 공식은 다음과 같다. 

float3 lightVec = -L.Direction;
float diffuseFactor = dot(lightVec, normal);
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;

 

Specular(경사)은 물체의 표면에서 특정 각도로 반사되어서 우리 눈에 보이는 조명이다. 반짝이는 효과라고 생각해도 된다. 물체의 재질에 따라 반짝임 정도가 달라진다.

계산은 L과 N의 반사각을 구한 다음 눈까지의 각도와 내적해준 다음 Specular값을 배수값으로 곱해주면 된다.

float3 v         = reflect(-lightVec, normal);
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);

spec    = specFactor * mat.Specular * L.Specular;

 

이렇게 해준 다음에 최종적으로  PS단계에서는 이 3가지 특성의 합으로 조명의 값을 연산해준다.

float4 litColor = ambient + diffuse + spec;

+ Recent posts