강의

https://inf.run/zKor1

 

학습 페이지

 

www.inflearn.com

 

 

오늘은 Normal Mapping 과 Displacement Mapping에 대해 학습하고 예제코드를 분석해보자.

우선 Normal Mapping은 물체를 정밀하게 표현하기 위해서는 삼각형의 개수를 늘리는 방법도 있지만 이 방법은 너무 부하가 많아진다. 그렇기 때문에 삼각형의 개수를 늘리지 않고 물체 표면의 Normal 값을 조정해주는 것으로 물체의 표면을 정밀하게 표현한다. 

이때 조정할 노멀 값을 모아서 노말 텍스처를 만들어주게 되는데 이때 텍스처에는 tagent Space 좌표계가 적용되어 있는데 이때 n(normal)값이 제일 많기 때문에 파란색으로 표현되곤 한다. 이 텍스처의 normal값을 추출해서 물체에 적용해주면 된다.

우선 코드를 실행시켜보면 왼쪽에는 Normal Mapping이 적용되지 않은 모습이고 오른쪽에는 Normal Mapping이 적용된 모습이다.

 

보통 노멀매핑은 24비트로 8비트씩 나눠서 3개의 좌표를 사용한다. 1바이트이기 때문에 각 값은 0~1사이의 값으로

표현한다. 이를 float -1~1까지의 값으로 치환해주는 공식을 통해 치환해주게 된다.

이때 tagent space라는 개념이 나오는데 이 정보는 매쉬의 정점에 포함된 정보로 이 좌표계를 기준으로 하는 x,y,z정보를 텍스처공간에 저장해주고 이 값을 통해 Normal Mapping을 해주는 것이다. 이때  정점 정보를 입력해줄 때 VertexIn 구조체의 정보와 같이 노멀정보와 탄젠트 정보를 넣어주어야한다. 

 

예제 코드에서 쉐이더 부분을 살펴보자면 노멀 정보를 가져와서 월드좌표로 바꿔주는 부분이 함수로 만들어져있고 이를 PS단계에서 호출하여 사용해주고 있다.

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

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

VertexOut VS(VertexIn vin)
{
    VertexOut vout;

    // Transform to world space space.
    vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
    vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
    vout.TangentW = mul(vin.TangentL, (float3x3)gWorld);

    // Transform to homogeneous clip space.
    vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);

    // Output vertex attributes for interpolation across triangle.
    vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;

    return vout;
}

Texture2D gNormalMap;

float3 normalMapSample = gNormalMap.Sample(samLinear, pin.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample, pin.NormalW, pin.TangentW);

 

월드좌표로 변환해줄 때 텍스처에서 UV좌표를 이용하여 특정 좌표를 구한다음에 그 값을 -1~1사이의 값으로 공식을 통해 변환해준 다음 월드좌표로 변환해주고 있다. 이때 이미 VS값에서 World를 구해줬지만 PS단계에서 보간때문에 normal과 tagent의 수직이 깨질 수 있기 때문에 이를 수정해주기 위해 탄젠트 값을 다시 연산을 해준 다음 탄젠트 스페이스에서 월드 스페이스로 변환해주고 있다.

//---------------------------------------------------------------------------------------
// Transforms a normal map sample to world space.
//---------------------------------------------------------------------------------------
float3 NormalSampleToWorldSpace(float3 normalMapSample, float3 unitNormalW, float3 tangentW)
{
	// Uncompress each component from [0,1] to [-1,1].
	float3 normalT = 2.0f * normalMapSample - 1.0f;

	// Build orthonormal basis.
	float3 N = unitNormalW;
	float3 T = normalize(tangentW - dot(tangentW, N) * N);
	float3 B = cross(N, T);

	float3x3 TBN = float3x3(T, B, N);

	// Transform from tangent space to world space.
	float3 bumpedNormalW = mul(normalT, TBN);

	return bumpedNormalW;
}

 

그리고 이렇게 변환된 값을  통해 빛연산을 해주고 있다.

// Sum the light contribution from each light source.  
[unroll]
for (int i = 0; i < gLightCount; ++i)
{
    float4 A, D, S;
    ComputeDirectionalLight(gMaterial, gDirLights[i], bumpedNormalW, toEye,
        A, D, S);

    ambient += A;
    diffuse += D;
    spec += S;
}

litColor = texColor * (ambient + diffuse) + spec;

if (gReflectionEnabled)
{
    float3 incident = -toEye;
    float3 reflectionVector = reflect(incident, bumpedNormalW);
    float4 reflectionColor = gCubeMap.Sample(samLinear, reflectionVector);

    litColor += gMaterial.Reflect * reflectionColor;
}

 

다음은 Displacement Mapping 표면에 굴곡과 균열을 묘사하는 높이 값을 넘겨주고 이를 통해 기하구조를 변경하는 것이다. 구현은 노멀매핑에서 w값을 height로 사용하거나 따로 텍스처를 만들어주면 된다. 왼쪽이 Normal Mapping만 적용된 화면이고 오른쪽이 Displacement Mapping까지 적용된 모습이다.

 

이 표면의 높이값을 실시간으로 조정해주기 위해서는 매쉬를 직접 조정해주는 방법도 있지만 이렇게 하면 부하가 심해지기 때문에 정점을 건드리지 않고 조정해줄 방법을 찾아야하는데 Tesellation 단계를 통해 조정을 해준다. 

 

코드를 보면 예제에서는 노멀 텍스처w값을 추출해서 사용하는 것을 볼 수 있다.

Texture2D gNormalMap;


PatchTess PatchHS(InputPatch<VertexOut, 3> patch,
    uint patchID : SV_PrimitiveID)
{
    PatchTess pt;

    // Average tess factors along edges, and pick an edge tess factor for 
    // the interior tessellation.  It is important to do the tess factor
    // calculation based on the edge properties so that edges shared by 
    // more than one triangle will have the same tessellation factor.  
    // Otherwise, gaps can appear.
    pt.EdgeTess[0] = 0.5f * (patch[1].TessFactor + patch[2].TessFactor);
    pt.EdgeTess[1] = 0.5f * (patch[2].TessFactor + patch[0].TessFactor);
    pt.EdgeTess[2] = 0.5f * (patch[0].TessFactor + patch[1].TessFactor);
    pt.InsideTess = pt.EdgeTess[0];

    return pt;
}

struct HullOut
{
    float3 PosW     : POSITION;
    float3 NormalW  : NORMAL;
    float3 TangentW : TANGENT;
    float2 Tex      : TEXCOORD;
};

[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("PatchHS")]
HullOut HS(InputPatch<VertexOut, 3> p,
    uint i : SV_OutputControlPointID,
    uint patchId : SV_PrimitiveID)
{
    HullOut hout;

    // Pass through shader.
    hout.PosW = p[i].PosW;
    hout.NormalW = p[i].NormalW;
    hout.TangentW = p[i].TangentW;
    hout.Tex = p[i].Tex;

    return hout;
}



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

//테셀레이션- 최종 위치결정
[domain("tri")]
DomainOut DS(PatchTess patchTess,
    float3 bary : SV_DomainLocation,
    const OutputPatch<HullOut, 3> tri)
{
    DomainOut dout;

    dout.PosW = bary.x * tri[0].PosW + bary.y * tri[1].PosW + bary.z * tri[2].PosW;
    dout.NormalW = bary.x * tri[0].NormalW + bary.y * tri[1].NormalW + bary.z * tri[2].NormalW;
    dout.TangentW = bary.x * tri[0].TangentW + bary.y * tri[1].TangentW + bary.z * tri[2].TangentW;
    dout.Tex = bary.x * tri[0].Tex + bary.y * tri[1].Tex + bary.z * tri[2].Tex;

    dout.NormalW = normalize(dout.NormalW);



    const float MipInterval = 20.0f;
    float mipLevel = clamp((distance(dout.PosW, gEyePosW) - MipInterval) / MipInterval, 0.0f, 6.0f);

    //텍스처에서 height 추출
    float h = gNormalMap.SampleLevel(samLinear, dout.Tex, mipLevel).a;

    //포지션 조정
    dout.PosW += (gHeightScale * (h - 1.0)) * dout.NormalW;

      //포지션 조정
    dout.PosH = mul(float4(dout.PosW, 1.0f), gViewProj);

    return dout;
}


오늘은 Sky Box에 대해 코드를 보며 학습해보자.

우선 예제코드를 실행해보면 풍경이 담긴 Sky Box가 적용되어 있는 모습을 볼 수 있다.

 

전에 Sky Box를 구현할 때는 커다란 구를 만들어두고 여기에 텍스처를 붙이고 우리가 안에서 그 텍스처를 보는 방식으로 

구현했었다. 이때 안쪽을 볼 수 있게 하기 위해 기존에 컬링으로 후면을 제거해줬는데 이를 없애고 전면을 없애는 방식으로 바꿔주어야 했고 카메라가 움직임에 따라 따라가게 해야했다. 그리고 스카이박스는 깊이를 1에 가깝게 만들어서 물체가

그려질 수 있고 Sky Box가 제일 끝에 위치하도록 만들어 줬다.

 

지금이 예제에는 구가 아닌 큐브를 통해 6면에 텍스처를 붙여주는 것으로 Sky Box를 구현해주고 있다. 이를 위해 상하좌우 풍경에 따라 다른 텍스처를 매핑해줘야한다. 이렇게 해주기 위해 3D Look up Vector를 사용하여 매핑해준다. 이 벡터를 활용하는 방법은 중심에서 가리키는 벡터에 따라 그려주는 연산을 해준다.

 

우선 쉐이더 코드를 살펴보자. 텍스처를 받아오는 변수로 TextureCube를 통해 받아오고 있으며 이를 PS단계에서 Sampler 연산을 해줄 때 pin.PosL가 Look up Vector이다. 

TextureCube gCubeMap;

float4 PS(VertexOut pin) : SV_Target
{
	return gCubeMap.Sample(samTriLinearSam, pin.PosL);
}

 

그리고 이 pin값은 VS단계에서 위치를 xyww로 밀어주고 있다. 이는 깊이값이 정확하게 1이 되도록 해주는 것이다. 이를 통해 SkyBox가 제일 뒤에 있어서 물체가 그려질 수 있다. 그리고 원점을 기준으로하는 로컬좌표를 넘겨주는 것으로 로컬좌표를 통해 각 정점의 위치로의 벡터도 나중에 만들어 줄 수 있게 한다.

VertexOut VS(VertexIn vin)
{
	VertexOut vout;

	// Set z = w so that z/w = 1 (i.e., skydome always on far plane).
	vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj).xyww;

	// Use local vertex position as cubemap lookup vector.
	vout.PosL = vin.PosL;

	return vout;
}

 

그리고 밑의 코드를 보면 컬링을 꺼주고 원래 깊이가 1인 오브젝트도 그려줄 수 있도록 옵션 설정을 해주고 있다.

RasterizerState NoCull
{
	CullMode = None;
};

DepthStencilState LessEqualDSS
{
	// Make sure the depth function is LESS_EQUAL and not just LESS.  
	// Otherwise, the normalized depth values at z = 1 (NDC) will 
	// fail the depth test if the depth buffer was cleared to 1.
	DepthFunc = LESS_EQUAL;
};

 

이렇게 해주면 큐브 매핑을 통한 기본적인 Sky Box를 구현할 수 있다.

 

다음으로 다이나믹 큐브맵에 대해 알아보자. 다이나믹 큐브맵을 주변 환경을 반사하는 모습을 표현할 때 자주 활용된다.

우선 예제코드를 실행해보면 가운데 구에 주변 환경이 반사되어 보이는 것을 볼 수 있다.

 

이것을 구현하는 것은 스텐실을 활용해서 실시간으로 여섯면을 만들어줘야한다. Dynamic Cube라고 했지만 구 모양을 그려주고 있는데 이는 큐브모양을 look up vector를 통해 오려붙여서 구를 만들어 줄 수 가 있다. 구에 반사되는 모습은 카메라를 원점에 위치시키고 반사 공식을 통해 구한 각 6면의 장면을 텍스처로 만든 다음 이를 적용 시켜 주면 된다.

 

코드를 살펴보면 동적으로 큐브맵 텍스처를 만들기 위해 각 장면을 화면이 아닌 텍스처에 먼저 그려주기 위한 변수가 있다. 그리고 원래 DrawScene이 원래 Scene을 그리지만 당장 화면에 그려주는 것이 아니라 텍스처로 그려줄 수 있다는 것을 볼 수 있다. 

이때 구의 각면에 적용할 DepthStencilView도 따로 적용해주고 있다. 이는 구에 적용하는 해상도에 맞춰서 적용해줘야하기 때문이다.

//각 장면을 텍스처로 저장하기 위한 변수
ComPtr<ID3D11RenderTargetView> _dynamicCubeMapRTV[6];

//6면의 카메라
Camera _cubeMapCamera[6];


void DynamicCubeMapDemo::DrawScene()
{
	ID3D11RenderTargetView* renderTargets[1];

	// 각 면을 텍스처로 저장
	_deviceContext->RSSetViewports(1, &_cubeMapViewport);
	for (int i = 0; i < 6; ++i)
	{
		// Clear cube map face and depth buffer.
		_deviceContext->ClearRenderTargetView(_dynamicCubeMapRTV[i].Get(), reinterpret_cast<const float*>(&Colors::Silver));
		_deviceContext->ClearDepthStencilView(_dynamicCubeMapDSV.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

		// Bind cube map face as render target.
		renderTargets[0] = _dynamicCubeMapRTV[i].Get();
		_deviceContext->OMSetRenderTargets(1, renderTargets, _dynamicCubeMapDSV.Get());

		// Draw the scene with the exception of the center sphere to this cube map face.
		DrawScene(_cubeMapCamera[i], false);
	}

	// 화면에 그려줄 수 있도록 렌더타켓 초기화
	_deviceContext->RSSetViewports(1, &_viewport);
	renderTargets[0] = _renderTargetView.Get();
	_deviceContext->OMSetRenderTargets(1, renderTargets, _depthStencilView.Get());

	// Have hardware generate lower mipmap levels of cube map.
	_deviceContext->GenerateMips(_dynamicCubeMapSRV.Get());

	// 구를 포함한 전체 그려주기
	_deviceContext->ClearRenderTargetView(_renderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Silver));
	_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	DrawScene(_camera, true);

	HR(_swapChain->Present(0, 0));
}

 

 

여기서 핵심은 UV매핑이 아닌 Look up Vector를 활용하여 텍스처를 그려줄 수 있고 이를 통해 환경이나 거울 같은 오브젝트를 그려줄 때도 사용할 수 있다는 것을 알 수 있다.


오늘은 Picking에 대한 내용을 복습하고 예제코드를 분석해보자.

우선 예제코드를 실행해보면 

자동차 오브젝트가 있다. 이 자동차 오브젝트를 우클릭해보면 클릭하는 면에 클릭이 적용되고 있는 모습을 볼 수 있다.

 

피킹은 마우스 커서로 우리가 찍은 좌표를 통해 물체를 파악하는 것이다. 이것이 중요한 이유는 3D 게임에서 클릭을 한다고 했을 때 2D화면 좌표계에서 선택한 좌표를 3D로 변환줘야한다. 이를 통해 어떤 것을 클릭해주는 지 알 수 있기 때문이다.

 

피킹 연산은 우리가 물체를 그려주기 위해 했던 로컬,월드,뷰,프로젝션,NDC,Screen까지의 연산의 반대로 해주면 된다.

이때 매쉬랑 충돌 판정을 한다는 것은 매쉬의 모든 삼각형을 순회하면서 레이케스팅을 해주는 것이다.

만약 예제코드와 같이 자동차안에서도 삼각형으로 분할되어 있는 세밀한 부분에 티킹 연산을 해준다고 하면 기존 오브젝트가 있는 로컬좌표로 클릭한 스크린 좌표를 변환해서 연산해주는 것이  더 좋을 것이다.

피킹연산은 CPU에서 이루어 진다. 월드 변환은 GPU에서 이루어 지고 이에 관한 정보도 GPU에서 가지고 있다. 그렇기 때문에 CPU에서는 피킹 연산에서  각 월드 좌표를 가지고 있지 않고 연산량이 적은 좌표계로 변환해서 연산해주면 된다. 

즉 레이케스트의 광선을 오브젝트의 로컬로 변환해서 연산해주면 되는 것이다.

 

그리고 연산에서 먼저 세밀한 부분에 피킹 연산을 해주기 보다는 충돌 영역을 지정해두고 여기에 충돌하면 2단계로 세밀한 부분에 피킹 연산을 해주는 것으로 최적화 해줄 수 있다.

 

코드를 보면 광선을 로컬좌표로 변환하고 있고 이를 통해 레이케스팅 연산을 해주고 있다. 이때 위에서 이야기한대로 먼저 대략적인 충돌영역으로 충돌처리를 해주고 만약 충돌했다면 2단계로 모든 삼각형에 순회를 해주며 충돌 연산을 해주고 있다. 그리고 피킹을 했을 때 가장 가까이 있는 오브젝트에 피킹을 해주어야하기 때문에 이러한 값을 추가해서 검사해주는 것을 볼 수 있다.

void PickingDemo::Pick(int32 sx, int32 sy)
{
	XMMATRIX P = _camera.Proj();

	Matrix m = P;
	// Compute picking ray in view space.
	float vx = (+2.0f * sx / _clientWidth - 1.0f) / m(0, 0); // P(0, 0);
	float vy = (-2.0f * sy / _clientHeight + 1.0f) / m(1, 1); // P(1, 1);

	// 뷰좌표계에서 광선정의
	XMVECTOR rayOrigin = ::XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
	XMVECTOR rayDir = ::XMVectorSet(vx, vy, 1.0f, 0.0f);

	// 광선을 로컬좌표로 변환
	XMMATRIX V = _camera.View();
	XMVECTOR D1 = ::XMMatrixDeterminant(V);
	XMMATRIX invView = ::XMMatrixInverse(&D1, V);

	XMMATRIX W = ::XMLoadFloat4x4(&_meshWorld);
	XMVECTOR D2 = ::XMMatrixDeterminant(W);
	XMMATRIX invWorld = ::XMMatrixInverse(&D2, W);

	XMMATRIX toLocal = ::XMMatrixMultiply(invView, invWorld);

	rayOrigin = ::XMVector3TransformCoord(rayOrigin, toLocal);
	rayDir = ::XMVector3TransformNormal(rayDir, toLocal);

	// Make the ray direction unit length for the intersection tests.
	rayDir = ::XMVector3Normalize(rayDir);

	// If we hit the bounding box of the Mesh, then we might have picked a Mesh triangle,
	// so do the ray/triangle tests.
	//
	// If we did not hit the bounding box, then it is impossible that we hit 
	// the Mesh, so do not waste effort doing ray/triangle tests.

	// Assume we have not picked anything yet, so init to -1.
	_pickedTriangle = -1;
	float tmin = 0.0f;

	if (_meshBox.Intersects(rayOrigin, rayDir, tmin))
	{
		// Find the nearest ray/triangle intersection.
		tmin = MathHelper::Infinity;
		for (UINT i = 0; i < _meshIndices.size() / 3; ++i)
		{
			// Indices for this triangle.
			UINT i0 = _meshIndices[i * 3 + 0];
			UINT i1 = _meshIndices[i * 3 + 1];
			UINT i2 = _meshIndices[i * 3 + 2];

			// Vertices for this triangle.
			XMVECTOR v0 = ::XMLoadFloat3(&_meshVertices[i0].pos);
			XMVECTOR v1 = ::XMLoadFloat3(&_meshVertices[i1].pos);
			XMVECTOR v2 = ::XMLoadFloat3(&_meshVertices[i2].pos);

			// We have to iterate over all the triangles in order to find the nearest intersection.
			float t = 0.0f;

			if (TriangleTests::Intersects(rayOrigin, rayDir, v0, v1, v2, t))
			{
				if (t < tmin)
				{
					// 가장 가까운 삼각형체크
					tmin = t;
					_pickedTriangle = i;
				}
			}
		}
	}
}

 

오늘은 Instancing과 Culling에 대해 복습해보고 예제코드를 분석해보자

 

우선 Instancing이 왜 필요할까에 대해 생각해보자. 각각의 오브젝트를 개별로 그려준다고 했을 때, 똑같은 오브젝트가 있더라도 다른 오브젝트가 그려주고 다시 똑같은 오브젝트를 그려준다고 하면 그려주기 위한 비용이 다시 생기기 때문에

드로우콜이 많이 발생한다. 효율적으로 그려주기 위해서는 같은 물체라면 한번에 다 그려주고 다른 오브젝트를 그려주는 것이 좋다. 이때 같은 물체라는 것은 같은 쉐이더와 머테리얼로 그려주는 물체를 의미한다.

 

우선 예제코드를 실행시켜보자. 실행화면 위쪽에 보면 125개의 물체 중에 13개만 보이고 있다는 것을 알 수 있다. 여기서 만약 2번을 눌러주면 인스턴싱모드가 꺼지면서 125개가 다 그려지며 프레임이 낮아지는 것을 볼 수 있다. 이때 13개가 보인다는 것은 컬링과 관련이 있다.

 

이때 같은 물체에서 달라지는 부분은 position과 같은 부분으로 이 연산은 쉐이더 코드의 VS부분의 구조체를 보면 된다. 아래에  변수가 VS단계에 입력으로 들어오는 부분에 인스턴싱관련된 부분이다.

struct VertexIn
{
    float3 PosL     : POSITION;
    float3 NormalL  : NORMAL;
    float2 Tex      : TEXCOORD;
    // 인스턴싱
    row_major float4x4 World  : WORLD;
    float4 Color    : COLOR;
    uint InstanceId : SV_InstanceID;
};

 

코드 상에서는 Vertex부분에 쉐이더코드에 맞게 묘사해주도록 변수가 맞춰져있다.

typedef struct D3D11_INPUT_ELEMENT_DESC
{
    LPCSTR SemanticName;
    UINT SemanticIndex;
    DXGI_FORMAT Format;
    UINT InputSlot;			//인스턴싱일때 구분해줘야함
    UINT AlignedByteOffset;
    D3D11_INPUT_CLASSIFICATION InputSlotClass;
    UINT InstanceDataStepRate;
} 	D3D11_INPUT_ELEMENT_DESC;
    
static ComPtr<ID3D11InputLayout> InstancedBasic32;
    
const D3D11_INPUT_ELEMENT_DESC InputLayoutDesc::InstancedBasic32[8] =
{
	{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
	{"NORMAL",   0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
	{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0},
	{ "WORLD", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1 },
	{ "WORLD", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D11_INPUT_PER_INSTANCE_DATA, 1 },
	{ "WORLD", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D11_INPUT_PER_INSTANCE_DATA, 1 },
	{ "WORLD", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D11_INPUT_PER_INSTANCE_DATA, 1 },
	{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 64,  D3D11_INPUT_PER_INSTANCE_DATA, 1 }
};

 

이때 D3D11_INPUT_PER_INSTANCE_DATA 뒤에 나오는 1이라는 수는 instance data step이라는 옵션으로 instance별 자료 원소 하나당 그릴 instance의 개수이다. 이렇게 묘사해준 값을 IA단계에서 넘겨줘야한다. IASetVertexBuffers에서 2번째 매개변수를 2로 설정해주고 있는 것을 볼 수 있다. 이 2는 2가지 값을 넘겨주고 있다는 것인데 이 2가지 값은 SkullVertexBuffer와 InstanceBuffer이다.

이렇게 해준다음 마지막에 DrawIndexedInstanced로 그려주면 된다. 이때 몇개를 그려줄지에 대한 값도 넘겨줘야한다.

ID3D11Buffer* vbs[2] = { _skullVB.Get(), _instancedBuffer.Get() };

_deviceContext->IASetVertexBuffers(0, 2, vbs, stride, offset);
_deviceContext->IASetIndexBuffer(_skullIB.Get(), DXGI_FORMAT_R32_UINT, 0);

_deviceContext->DrawIndexedInstanced(_skullIndexCount, _visibleObjectCount, 0, 0, 0);

이때 InstanceBuffer를 묘사해주는 부분을 가보면 InstanceData를 통해 데이터를 채워주고 이 값에 따라 InstanceBuffer를 채워준다.

void InstancingAndCullingDemo::BuildInstancedBuffer()
{
	const int32 n = 5;
	_instancedData.resize(n * n * n);

	float width = 200.0f;
	float height = 200.0f;
	float depth = 200.0f;

	float x = -0.5f * width;
	float y = -0.5f * height;
	float z = -0.5f * depth;
	float dx = width / (n - 1);
	float dy = height / (n - 1);
	float dz = depth / (n - 1);

	for (int k = 0; k < n; ++k)
	{
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				// Position instanced along a 3D grid.
				_instancedData[k * n * n + i * n + j].World = XMFLOAT4X4(
					1.0f, 0.0f, 0.0f, 0.0f,
					0.0f, 1.0f, 0.0f, 0.0f,
					0.0f, 0.0f, 1.0f, 0.0f,
					x + j * dx, y + i * dy, z + k * dz, 1.0f);

				// Random color.
				_instancedData[k * n * n + i * n + j].Color.x = MathHelper::RandF(0.0f, 1.0f);
				_instancedData[k * n * n + i * n + j].Color.y = MathHelper::RandF(0.0f, 1.0f);
				_instancedData[k * n * n + i * n + j].Color.z = MathHelper::RandF(0.0f, 1.0f);
				_instancedData[k * n * n + i * n + j].Color.w = 1.0f;
			}
		}
	}

	D3D11_BUFFER_DESC vbd;
	vbd.Usage = D3D11_USAGE_DYNAMIC;
	vbd.ByteWidth = sizeof(InstancedData) * _instancedData.size();
	vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	vbd.MiscFlags = 0;
	vbd.StructureByteStride = 0;

	HR(_device->CreateBuffer(&vbd, 0, _instancedBuffer.GetAddressOf()));
}

 

 Rasterizer에서 실행되는 것으로 영역에서 벗어난 부분이라면 보이지 않게 하는 데 이 부분에서 컬링이 실행되긴한다.

하지만 그 앞 단계까지에서 그려줄 애들은 이미 계산을 해준 상태이기 때문에 만약 안그려줘도 되는 아이라면 CPU단에서 처리해주는 것이 효율적일 것이다. 이를 해주는 것이 절두체 컬링(Frustum Culling)이다.

 

아래 코드를 보면 2개의 변수가 각각 해골이 그려져있는 영역과 카메라의 절두체 영역으로 절두체 컬링을 위한 것이다. 

//해골 영역
BoundingBox _skullBox;
//절두체 영역
BoundingFrustum _camFrustum;

 

이를 통해 카메라의 절두체 안에 있는지 체크하고 만약 있다면 그려주는 코드는 UpdateScene에 있다.

void InstancingAndCullingDemo::UpdateScene(float dt)
{
if (_frustumCullingEnabled)
{
	XMVECTOR detView = XMMatrixDeterminant(_camera.View());
	XMMATRIX invView = XMMatrixInverse(&detView, _camera.View());

	D3D11_MAPPED_SUBRESOURCE mappedData;
	_deviceContext->Map(_instancedBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);

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

	for (uint32 i = 0; i < _instancedData.size(); ++i)
	{
		XMMATRIX W = ::XMLoadFloat4x4(&_instancedData[i].World);
		XMVECTOR D = ::XMMatrixDeterminant(W);
		XMMATRIX invWorld = ::XMMatrixInverse(&D, W);

		// View space to the object's local space.
		XMMATRIX toLocal = ::XMMatrixMultiply(invView, invWorld);

		// Decompose the matrix into its individual parts.
		XMVECTOR scale;
		XMVECTOR rotQuat;
		XMVECTOR translation;
		::XMMatrixDecompose(&scale, &rotQuat, &translation, toLocal);

		// Transform the camera frustum from view space to the object's local space.
		BoundingFrustum localspaceFrustum;

		_camFrustum.Transform(localspaceFrustum, XMVectorGetX(scale), rotQuat, translation);
		//XNA::TransformFrustum(&localspaceFrustum, &_camFrustum, );

		// 그려줘야하는지 체크
		if (localspaceFrustum.Contains(_skullBox))
		{
			// Write the instance data to dynamic VB of the visible objects.
			dataView[_visibleObjectCount++] = _instancedData[i];
		}
	}

	_deviceContext->Unmap(_instancedBuffer.Get(), 0);
}
}

 

절두체 판단에서는 한면에 대해서 이 물체가 절두체 이전에 있는지 이후에 있는지를 통해 그려주는지 판단해주고 이를 6면에 대해서 해주면 된다. 이 중에 하나라도 통과하지 못하면 그려주지 않는다.


Tessellation영역은 파이프라인에서 Hull Shader,Tessellator, Domain Shader를 합친 것이다. 이때 Hull Shader와 Domain Shader는 쉐이더 코드를 통해 사용할 수 있고 Tessellator는 옵션을 통해 사용해줄 수 있다.

 

Tessellatoion은 기하 구조를 더 작은 삼각형들로 분할하고 새로 생긴 정점들의 위치를 적절한 방식으로 조절해주는 것이다. 주로 지형 지물, 터레인에서 많이 사용되고 특히 GPU에서의 LOD(멀수록 퀄리티 떨어져 보임)연산에서 연관이 많다.

 

이 Tessellatoion을 사용할 때는 삼각형을 매개변수로 사용하는 것이 아닌 D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST 이런 변수를 입력으로 받는다. 이것은 4개의 제어점으로 이것을 분할해서 사용하는 것이다. Tessellation은 이 제어점을 기준으로 하는 패치가 기본 단위로 연산이 이루어 진다.

예제코드를 실행하게 되면 멀어질수록 삼각형이 적어지고 가까이 갈수록 삼각형의 개수가 많아져서 세밀하게 보이게 된다.

 

쉐이더 코드를 살펴보면 기존의 VS와 PS단계에서 추가된 코드를 볼 수 있다. 우선 Hull Shader 코드에서  각 Tessellation의 기본 단위인 패치마다 실행되는 ConstantHS 단계를 먼저 살펴보자. 이 단계에서 얼마나 더 세분화해줄 지에 관련된 계수를 출력해준다. 이 계수에 해당하는 변수는 각 변의 세분정도를 나타내는 EdgeTess와 내부의 분할정도를 나타내는 InsideTess가 있다.

struct PatchTess
{
	float EdgeTess[4]   : SV_TessFactor;
	float InsideTess[2] : SV_InsideTessFactor;
};

PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
{
	PatchTess pt;

	//거리계산 - 패치들의 평균값
	float3 centerL = 0.25f * (patch[0].PosL + patch[1].PosL + patch[2].PosL + patch[3].PosL);
	float3 centerW = mul(float4(centerL, 1.0f), gWorld).xyz;

	//패치들과 카메라간의 거리 파악
	float d = distance(centerW, gEyePosW);


	//20(최소)~100(최대)사이
	const float d0 = 20.0f;
	const float d1 = 100.0f;
	float tess = 64.0f * saturate((d1 - d) / (d1 - d0));

	//각 변의 세분정도
	pt.EdgeTess[0] = tess;
	pt.EdgeTess[1] = tess;
	pt.EdgeTess[2] = tess;
	pt.EdgeTess[3] = tess;

	//내부의 분할정도
	pt.InsideTess[0] = tess;
	pt.InsideTess[1] = tess;

	return pt;
}

 

이 다음 단계는 출력하는 제어점마다 한번씩 실행되는 HS 단계로 표면을 어떻게 표현할지를 정해준다. 현재는 그냥 통과 시켜주고 있지만 설정해줄 수 있는 부분이 많다.

struct HullOut
{
	float3 PosL : POSITION;
};

[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(4)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
//출력하는 제어점마다 한번씩- 표면
HullOut HS(InputPatch<VertexOut, 4> p,
	uint i : SV_OutputControlPointID,
	uint patchId : SV_PrimitiveID)
{
	HullOut hout;

	hout.PosL = p[i].PosL;

	return hout;
}

 

그 다음이 Tessellator 단계이다. 이 단계에서는 쉐이더에서 정해준 값에 따라 분할이 일어나는 단계로 GPU가 해주는 작업이고 Domain shader단계로 넘어가게 된다. 이 단계에서는 동적으로 생성과 기존 정점의 행렬연산을 해주게 된다. Tesselation의 VS단계라고 보면 된다.

struct DomainOut
{
	float4 PosH : SV_POSITION;
};

// The domain shader is called for every vertex created by the tessellator.  
// It is like the vertex shader after tessellation.
[domain("quad")]
DomainOut DS(PatchTess patchTess,
	float2 uv : SV_DomainLocation,
	const OutputPatch<HullOut, 4> quad)
{
	DomainOut dout;

	// Bilinear interpolation.
	float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x);
	float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x);
	float3 p = lerp(v1, v2, uv.y);

	// Displacement mapping
	p.y = 0.3f * (p.z * sin(p.x) + p.x * cos(p.z));

	dout.PosH = mul(float4(p, 1.0f), gWorldViewProj);

	return dout;
}

 

Tesselation은 GS단계와 비슷하지만 하나의 물체가 있는 상태에서 분할하는 느낌이고 GS단계는 별도의 물체를 생성할 수도 있는 느낌이라고 생각하면 된다. GS는 파티클이나 빌보드에 많이 사용되고 Tesselation은 Terrain같은 지형지물에 많이 사용한다.

 

오늘은 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;
}

 

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

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

정점 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);

 

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

우선 실행을 해보면 키보드에서 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();

 

+ Recent posts