OpenGL/개발 일지

[OpenGL] 게임 엔진 개발 (8) - Terrain

ciel45 2024. 8. 12. 21:50

참조 : https://learnopengl.com/Guest-Articles/2021/Tessellation/Tessellation

 

LearnOpenGL - Tessellation

Tessellation Guest-Articles/2021/Tessellation/Tessellation Tessellation Chapter II: Rendering Terrain using Tessellation Shaders & Dynamic Levels of Detail In order to complete this chapter, you will need to be able to create an OpenGL 4.0+ context. This s

learnopengl.com

learnopengl.com에 테셀레이션을 활용하여 터레인을 구현하는 글이 있어, 많은 도움을 받았다.

 

 

(본 글의 원활한 이해를 위해서는 아래 포스팅을 먼저 보고 오시는 것을 추천드립니다)

https://ciel45.tistory.com/121

 

[OpenGL] Tessellation Shader

참조 : https://learnopengl.com/Guest-Articles/2021/Tessellation/Tessellation LearnOpenGL - TessellationTessellation Guest-Articles/2021/Tessellation/Tessellation Tessellation Chapter II: Rendering Terrain using Tessellation Shaders & Dynamic Levels of

ciel45.tistory.com

 

 

테셀레이션 사용의 장점은 CPU의 부담을 많이 덜어준다는 것이다.

 

원래 터레인에는 굉장히 많은 양의 버텍스가 들어가고, 정석대로라면 CPU에서 버텍스를 하나하나 준비해서 GPU로 쏴줘야하지만, 

 

GPU가 굳이 모든 버텍스를 받지 않고, 즉 폴리곤을 대충 덩어리로 받고 본인이 렌더링 파이프라인 과정에서 알아서 쪼개기로 한다면 CPU는 굳이 많은 양의 버텍스를 쏴줄 필요가 없는 것이다.

 

아래는 해당 기술을 사용하는 Terrain 코드이다.

 

Terrain.cpp:

void Terrain::LoadTerrain(const char* fileName)
{
    // shader setup
    terrainShader = new Shader();
    terrainShader->CreateFromFiles("Shaders/terrainVertex.glsl", "Shaders/terrainFragment.glsl", nullptr,
                                "Shaders/terrainTC.glsl", "Shaders/terrainTE.glsl");
    // -------------------------------------------------------------------------
    // height map 로드
    heightMap = new Texture(fileName);
    heightMap->LoadTexture(4);
    // -------------------------------------------------------------------------
    // diffuse map 로드    
    diffuseMap = new Texture("Textures/aerial_grass_rock_4k.blend/aerial_grass_rock_diff_4k.jpg");
    diffuseMap->LoadTexture(4);
    // -------------------------------------------------------------------------

    // vertex generation
    rez = 100;
    width = heightMap->GetWidth();
    height = heightMap->GetHeight();
    for (unsigned i = 0; i < rez; i++)
    {
        for (unsigned j = 0; j < rez; j++)
        {
            Vertex vertexA;
            vertexA.Position.x = -width / 2.0f + width * i / (float)rez;
            vertexA.Position.y = 0.f;
            vertexA.Position.z = -height / 2.0f + height * j / (float)rez;
            vertexA.TexCoords.x = i / (float)rez;
            vertexA.TexCoords.y = j / (float)rez;
            vertices.push_back(vertexA);

            Vertex vertexB;
            vertexB.Position.x = -width / 2.0f + width * (i + 1) / (float)rez;
            vertexB.Position.y = 0.f;
            vertexB.Position.z = -height / 2.0f + height * j / (float)rez;
            vertexB.TexCoords.x = (i + 1) / (float)rez;
            vertexB.TexCoords.y = j / (float)rez;
            vertices.push_back(vertexB);

            Vertex vertexC;
            vertexC.Position.x = -width / 2.0f + width * i / (float)rez;
            vertexC.Position.y = 0.f;
            vertexC.Position.z = -height / 2.0f + height * (j + 1) / (float)rez;
            vertexC.TexCoords.x = i / (float)rez;
            vertexC.TexCoords.y = (j + 1) / (float)rez;
            vertices.push_back(vertexC);

            Vertex vertexD;
            vertexD.Position.x = -width / 2.0f + width * (i + 1) / (float)rez;
            vertexD.Position.y = 0.f;
            vertexD.Position.z = -height / 2.0f + height * (j + 1) / (float)rez;
            vertexD.TexCoords.x = (i + 1) / (float)rez;
            vertexD.TexCoords.y = (j + 1) / (float)rez;
            vertices.push_back(vertexD);
        }
    }

    std::cout << "Loaded " << rez * rez << " patches of 4 control points each" << std::endl;
    std::cout << "Processing " << rez * rez * 4 << " vertices in vertex shader" << std::endl;

    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * vertices.size(), &vertices[0], GL_STATIC_DRAW);

    // position
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
    glEnableVertexAttribArray(0);

    // texture
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);

    glPatchParameteri(GL_PATCH_VERTICES, 4);
}

rez라는 변수가 핵심인데, rez는 보낼 데이터(HeightMap의 정보)의 해상도를 의미한다.

원래 HeightMap의 width, height가 존재할 때 width * height 개의 버텍스(width와 height는 대개 1000을 넘는다)를 보내야 하는데 그럴 필요 없이,

대략적으로 rez * rez개의 버텍스만 보내는 것이다.

 

 

다음은 쉐이더 코드들인데, Vertex Shader, Tessellation Control Shader는 위의 learnopengl.com의 코드와 거의 비슷하다.

 

Tessellation Evaluation Shader는 다음과 같다.

#version 460 core

layout (quads, fractional_odd_spacing, ccw) in;

uniform sampler2D heightSampler;
uniform mat4 PVM;
uniform mat4 modelMat;
uniform float HEIGHT_SCALE;
uniform float HEIGHT_SHIFT;

in vec2 TextureCoord[];

out float Height;
out vec2 TexCoord_global;
out vec2 TexCoord_local;
out vec3 FragPos;

void main()
{
	float u = gl_TessCoord.x;
	float v = gl_TessCoord.y;

	vec2 t00 = TextureCoord[0];
	vec2 t01 = TextureCoord[1];
	vec2 t10 = TextureCoord[2];
	vec2 t11 = TextureCoord[3];

	vec2 t0 = mix(t00, t01, u);
	vec2 t1 = mix(t10, t11, u);
	vec2 texCoord = mix(t0, t1, v);

	Height = texture(heightSampler, texCoord).x * HEIGHT_SCALE + HEIGHT_SHIFT;

	vec4 p00 = gl_in[0].gl_Position;
	vec4 p01 = gl_in[1].gl_Position;
	vec4 p10 = gl_in[2].gl_Position;
	vec4 p11 = gl_in[3].gl_Position;

	vec4 uVec = p01 - p00;
	vec4 vVec = p10 - p00;
	vec4 normal = normalize(vec4(cross(vVec.xyz, uVec.xyz), 0));

	vec4 p0 = mix(p00, p01, u);
	vec4 p1 = mix(p10, p11, u);
	vec4 p = mix(p0, p1, v);
	
	p += normal * Height;

	gl_Position = PVM * p;
	TexCoord_global = texCoord;
	TexCoord_local = gl_TessCoord.xy;
	FragPos = (modelMat * p).xyz;
}

TexCoord와 Position을 쭉쭉 Bilinear Interpolation 해주고 있다.

Position만 간단하게 그림으로 보자면 다음과 같다.

이런 식으로 분할점들에 해당하는 데이터를 얻는다.
적절한 TexCoord를 구해 HeightMap에서 Height를 떼어와 사용하고,
적절한 Position을 구해 gl_Position에 넣어주고 있는 것을 볼 수 있다.

 

맨 아래에 TexCoord_global과 TexCoord_local을 나누어 담았는데,

TexCoord_global은 전체 HeightMap 안에서의 텍스쳐 좌표,

TexCoord_local은 patch 안에서의 텍스쳐 좌표이다.

 

나누어 담은 이유는 diffuse 텍스쳐를 이용해 타일링을 할 때는 TexCoord_local을 사용하고,

HeightMap에서 텍셀 값을 읽어와 normal을 계산할 때는 TexCoord_global을 사용하고 위해서이다.

 

해당 작업을 수행하는 Fragment Shader는 다음과 같다.

#version 460 core

in float Height;
in vec2 TexCoord_global;
in vec2 TexCoord_local;
in vec3 FragPos;

out vec4 FragColor;

const int MAX_POINT_LIGHTS = 3;

struct Light
{
	vec4 color;
	float ambientIntensity;
	float diffuseIntensity;
};

struct DirectionalLight
{
	Light base;
	vec3 direction;
};

struct PointLight
{
	Light base;
	vec3 position;
	float constant;
	float linear;
	float exponent;
};

uniform sampler2D diffuseSampler;
uniform DirectionalLight directionalLight;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform int pointLightCount;

uniform vec2 texelSize;
uniform sampler2D heightSampler;
uniform float HEIGHT_SCALE;

vec4 CalcLightByDirection(Light light, vec3 direction, vec3 normal)
{
	direction = normalize(direction);
	normal = normalize(normal);

	vec4 ambientColor = (light.ambientIntensity * light.color);
	
	float diffuseFactor = max(dot(normal, direction), 0);
	vec4 diffuseColor = (light.diffuseIntensity * light.color) * diffuseFactor;

	return ambientColor + diffuseColor;
}

vec4 CalcDirectionalLight(vec3 normal)
{
	return CalcLightByDirection(directionalLight.base, directionalLight.direction, normal);
}

vec4 CalcPointLights(vec3 normal)
{
	vec4 totalColor = vec4(0,0,0,0);
	for(int i = 0; i < pointLightCount; i++)
	{
		vec3 direction = pointLights[i].position - FragPos;
		vec4 color = CalcLightByDirection(pointLights[i].base, direction, normal);

		float distance = length(pointLights[i].position - FragPos);

		float attenuation =	
			pointLights[i].exponent * distance * distance + 
			pointLights[i].linear * distance + 
			pointLights[i].constant;

		totalColor += (color / attenuation);
	}
	return totalColor;
}

void main()
{
    vec4 texColor = texture(diffuseSampler, TexCoord_local);

	// 인접한 텍셀 4개를 쭉 긁어와서 normal 계산
	float left  = texture(heightSampler, TexCoord_global + vec2(-texelSize.x, 0.0)).r * HEIGHT_SCALE * 2.0 - 1.0;
	float right = texture(heightSampler, TexCoord_global + vec2( texelSize.x, 0.0)).r * HEIGHT_SCALE * 2.0 - 1.0;
	float up    = texture(heightSampler, TexCoord_global + vec2(0.0,  texelSize.y)).r * HEIGHT_SCALE * 2.0 - 1.0;
	float down  = texture(heightSampler, TexCoord_global + vec2(0.0, -texelSize.y)).r * HEIGHT_SCALE * 2.0 - 1.0;
	vec3 normal = normalize(vec3(down - up, 2.0, left - right));

	vec4 finalColor = vec4(0,0,0,0);
	finalColor += CalcDirectionalLight(normal);
	finalColor += CalcPointLights(normal);	

	FragColor = texColor * finalColor;
	FragColor +=  Height/128.f;
}

마지막 줄의 코드는 그냥 높은 곳은 더 밝게 보이게 한 것이다.

 

이제 이 쉐이더를 이용하여 터레인을 그리는 코드는 다음과 같다.

void Terrain::DrawTerrain(glm::mat4 viewMat, glm::mat4 projMat)
{
    terrainShader->UseShader();

    heightMap->UseTexture(GL_TEXTURE0);
    terrainShader->setInt("heightSampler", 0);

    diffuseMap->UseTexture(GL_TEXTURE1);
    terrainShader->setInt("diffuseSampler", 1);
    
    glm::mat4 modelMat = GetModelMat();
    glm::mat4 PVM = projMat * viewMat * modelMat;
    loc_PVM = terrainShader->GetPVMLoc();
    glUniformMatrix4fv(loc_PVM, 1, GL_FALSE, glm::value_ptr(PVM));

    terrainShader->setMat4("modelViewMat", modelMat * viewMat);
    terrainShader->setMat4("modelMat", modelMat);

    glm::vec2 texelSize(1.f/width, 1.f/height);
    terrainShader->setVec2("texelSize", texelSize);

    terrainShader->setFloat("HEIGHT_SCALE", heightScale);
    terrainShader->setFloat("HEIGHT_SHIFT", heightShift);

    glBindVertexArray(VAO);
    glDrawArrays(GL_PATCHES, 0, 4*rez*rez);

    glBindVertexArray(0);
}

 

 

실행 결과: 

 

사용한 HeightMap:

 

영상에서 보이듯이 터레인을 가까이서 볼 수록 메시가 촘촘해지는 LOD가 적용되어있는데,

이건 맨 위 learnopengl.com 사이트에 설명이 너무 잘 되어있어서 여기서 설명은 생략하려고 한다.

Tessellation Control Shader 부분에 구현되어있다.

 

 

전체 소스코드 : https://github.com/sys010611/YsEngine

 

GitHub - sys010611/YsEngine

Contribute to sys010611/YsEngine development by creating an account on GitHub.

github.com