개발일지/소프트 렌더러

소프트웨어 렌더러 만들기 - 14 (텍스처링)

hwi.middle 2024. 7. 25. 15:10

여기까지의 작업내용: https://github.com/hwi-middle/HimchanSoftwareRenderer/tree/b7de9ae464050e28b4a0df087a3d53d1a5eaed63

 

GitHub - hwi-middle/HimchanSoftwareRenderer: C++로 구현한 소프트웨어 렌더러입니다.

C++로 구현한 소프트웨어 렌더러입니다. Contribute to hwi-middle/HimchanSoftwareRenderer development by creating an account on GitHub.

github.com

 

stb 라이브러리

png, jpg, tga 등 다양한 포맷을 이미지 라이브러리에 대해 찾아보니 stb 라이브러리 얘기가 많았고, CK렌더러도 stb를 사용하고 있다는 것을 알았다. 검색해보니 사용법도 매우 간단하여, stb를 사용하기로 했다.

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

사용을 위해서는 cpp 파일에서 이렇게 include 해줘야한다고 한다.

텍스처 샘플링

class WinRenderer
{
	// ...	
private:
    Color32* TextureBuffer;
    int TexWidth, TexHeight, TexChannels;
};

일단 임시로 WinRenderer 클래스에 텍스처 버퍼를 만들었다. 제대로 구조를 짜려면 리소스 관리 해주는 클래스에서 Texture 클래스를 가지고 있는 식으로 해야하는데 일단 텍스처링 구현이 우선이기 때문에 이렇게 구현해보겠다.

void WinRenderer::InitTextureBuffer()
{
	FILE* File = nullptr;
	const std::string FileName = "texture.png";
	unsigned char* LoadBuffer = stbi_load(FileName.c_str(), &TexWidth, &TexHeight, &TexChannels, 0);

	if (LoadBuffer == nullptr)
	{
		std::cout << "Failed to load texture\n";
		if (stbi_failure_reason())
		{
			std::cout << stbi_failure_reason();
		}

		return;
	}

	std::cout << "Texture loaded: " << TexWidth << "x" << TexHeight << " " << TexChannels << " channels\n";
	TextureBuffer = new Color32[TexWidth * TexHeight];
	for (int i = 0; i < TexWidth * TexHeight; ++i)
	{
		TextureBuffer[i].R = LoadBuffer[i * TexChannels];
		TextureBuffer[i].G = LoadBuffer[i * TexChannels + 1];
		TextureBuffer[i].B = LoadBuffer[i * TexChannels + 2];
		TextureBuffer[i].A = 255;
	}

	stbi_image_free(LoadBuffer);
}

그리고 이렇게 텍스처 버퍼를 초기화할 수 있다. RGBA 채널을 모두 갖는 텍스처를 가져온다는게 보장 되면 LoadBuffer 없이 TextureBuffer에 넣을 수도 있긴 한데(그리고 나서 R이랑 B swap해줘야함), jpg 같은 포맷도 있기 때문에 이렇게 구현했다.

 

Color32 WinRenderer::SampleTexture(const Vector2& InUV) const
{
	int X = Math::Clamp(static_cast<int>(InUV.X * TexWidth + 0.5f), 0, TexWidth - 1);
	int Y = Math::Clamp(static_cast<int>(InUV.Y * TexHeight + 0.5f), 0, TexHeight - 1);
	return TextureBuffer[Y * TexWidth + X];
}

Color32 WinRenderer::SampleTexture(const Vertex& InVertex) const
{
	return SampleTexture(InVertex.UV);
}

텍스처 샘플링은 우선 최근접점 이웃으로 선택했다.

void WinRenderer::DrawTriangle(const Vertex& InVertex1, const Vertex& InVertex2, const Vertex& InVertex3, const Color InColor)
{
	std::array<Vertex, 3> Vertices =
	{
		InVertex1,
		InVertex2,
		InVertex3
	};
	std::for_each(Vertices.begin(), Vertices.end(), [&](Vertex& InVertex) {
		InVertex.Position = ScreenPoint::CartesianToScreen(InVertex.Position, Width, Height);
		});
	std::sort(Vertices.begin(), Vertices.end(), [](const Vertex& InLhs, const Vertex& InRhs) { return InLhs.Position.Y < InRhs.Position.Y; });

	int32 X1 = Vertices[0].Position.X;
	int32 Y1 = Vertices[0].Position.Y;
	int32 X2 = Vertices[1].Position.X;
	int32 Y2 = Vertices[1].Position.Y;
	int32 X3 = Vertices[2].Position.X;
	int32 Y3 = Vertices[2].Position.Y;
	Vector2 UV1 = Vertices[0].UV;
	Vector2 UV2 = Vertices[1].UV;
	Vector2 UV3 = Vertices[2].UV;

	// Degenerate triangle
	if ((X2 - X1) * (Y3 - Y1) == (X3 - X1) * (Y2 - Y1))
	{
		return;
	}

	float A12 = Y1 != Y2 ? (X2 - X1) / static_cast<float>(Y2 - Y1) : 0.f;
	float A13 = (X3 - X1) / static_cast<float>(Y3 - Y1);
	float A23 = Y2 != Y3 ? (X3 - X2) / static_cast<float>(Y3 - Y2) : 0.f;

	Vector2 DeltaColorStart = (Y2 != Y1) ? (UV2 - UV1) / static_cast<float>(Y2 - Y1) : Vector2(0.f, 0.f);
	Vector2 DeltaColorEnd = (Y3 != Y1) ? (UV3 - UV1) / static_cast<float>(Y3 - Y1) : Vector2(0.f, 0.f);
	Vector2 UvStart = UV1;
	Vector2 UvEnd = UV1;

	for (int Y = Y1; Y <= Y2; ++Y)
	{
		int32 XStart = X1 + A12 * (Y - Y1);
		int32 XEnd = X1 + A13 * (Y - Y1);

		bool bIsSwapped = false;
		if (XStart > XEnd)
		{
			std::swap(XStart, XEnd);
			std::swap(UvStart, UvEnd);
			bIsSwapped = true;
		}

		XStart = Math::Clamp(XStart, 0, Width - 1);
		XEnd = Math::Clamp(XEnd, 0, Width - 1);
		for (int X = XStart; X <= XEnd; ++X)
		{
			Vector2 UV = Math::Lerp(UvStart, UvEnd, (X - XStart) / static_cast<float>(XEnd - XStart));
			SetPixel(X, Y, SampleTexture(UV));
		}

		if (bIsSwapped)
		{
			std::swap(UvStart, UvEnd);
		}

		UvStart += DeltaColorStart;
		UvEnd += DeltaColorEnd;
	}

	DeltaColorStart = (Y3 != Y2) ? (UV3 - UV2) / static_cast<float>(Y3 - Y2) : Vector2(0, 0);

	// 첫 번째 루프를 건너뛰는 경우가 있으므로 다시 계산
	UvStart = UV2;
	UvEnd = UV1 + DeltaColorEnd * (Y2 - Y1);

	for (int Y = Y2; Y <= Y3; ++Y)
	{
		int32 XStart = X2 + A23 * static_cast<float>(Y - Y2);
		int32 XEnd = X1 + A13 * static_cast<float>(Y - Y1);

		bool bIsSwapped = false;
		if (XStart > XEnd)
		{
			std::swap(XStart, XEnd);
			std::swap(UvStart, UvEnd);
			bIsSwapped = true;
		}

		XStart = Math::Clamp(XStart, 0, Width - 1);
		XEnd = Math::Clamp(XEnd, 0, Width - 1);
		for (int X = XStart; X <= XEnd; ++X)
		{
			Vector2 UV = Math::Lerp(UvStart, UvEnd, (X - XStart) / static_cast<float>(XEnd - XStart));
			SetPixel(X, Y, SampleTexture(UV));
		}

		if (bIsSwapped)
		{
			std::swap(UvStart, UvEnd);
		}

		UvStart += DeltaColorStart;
		UvEnd += DeltaColorEnd;
	}
}

그리고 이제 삼각형을 그릴 때 UV를 보간하고, UV에 따른 텍스처를 샘플링하도록 수정했다.

그리하여 출력한 이미지. RGB는 꼬이지 않게 잘 가져왔는지 등을 테스트하기 위해 만들어본 텍스처를 가져와봤다. stb라는 좋은 라이브러리가 있어서 생각보다 어렵지 않게 구현할 수 있었다.

앞으로의 계획

이제 2차원 공간을 3차원으로 확장할 차례다. 다만 그 전에 키 입력을 처리하는 부분을 구현하고 다시 돌아올 예정이다. 드디어 3차원 공간을 렌더링할 수 있겠구나. 조만간에 키 입력 처리 구현 후 3D 렌더러로 멋지게 돌아오도록 하겠다!