참조 : https://learnopengl.com/Guest-Articles/2021/Tessellation/Tessellation
learnopengl.com에 테셀레이션을 활용하여 터레인을 구현하는 글이 있어, 많은 도움을 받았다.
(본 글의 원활한 이해를 위해서는 아래 포스팅을 먼저 보고 오시는 것을 추천드립니다)
https://ciel45.tistory.com/121
테셀레이션 사용의 장점은 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
'OpenGL > 개발 일지' 카테고리의 다른 글
[OpenGL] 게임 엔진 개발 (10) - Terrain Collision (0) | 2024.08.20 |
---|---|
[OpenGL] 게임 엔진 개발 (9) - 플레이어 이동 (1) | 2024.08.20 |
[OpenGL] 게임 엔진 개발 (7) - Normal Mapping (0) | 2024.08.01 |
[OpenGL] 게임 엔진 개발 (6) - Scene Hierarchy (0) | 2024.07.30 |
[OpenGL] 게임 엔진 개발 (5) - Skeletal Animation (3) (0) | 2024.07.26 |