OpenGL/개발 일지

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

ciel45 2024. 8. 20. 20:09

플레이어가 이동할 수 있게 되었으므로, 터레인 위에서 자연스럽게 움직일 수 있도록 플레이어와 터레인 간 콜리전을 구현해볼 것이다.

 

사실 Player 클래스에는 이미 관련 코드가 짜여져 있다.

bool Player::Move(float deltaTime, Terrain* terrain)
{
	// 회전
	GLfloat* currRot = model->GetRotate();

	float rotation = currTurnSpeed * deltaTime;

	float newRotY = currRot[1] + rotation; // new rotY
	if(newRotY > 180)
		newRotY -= 360.f;
	if (newRotY < -180.f)
		newRotY += 360.f;

	glm::vec3 newRot(currRot[0], newRotY, currRot[2]);

	model->SetRotate(newRot);

	// 이동
	GLfloat* currPos = model->GetTranslate();
	float distance = currMoveSpeed * deltaTime;

	float dx = distance * sinf(glm::radians(newRotY));
	float dz = distance * cosf(glm::radians(newRotY));

	upwardSpeed -= GRAVITY * deltaTime;

	glm::vec3 newPos(currPos[0]+dx, currPos[1] + upwardSpeed, currPos[2]+dz);

	groundHeight = terrain->GetHeight(currPos[0], currPos[2]);
	if (newPos[1] <= groundHeight) // 땅에 닿았다면
	{
		upwardSpeed = 0;
		newPos[1] = groundHeight;
		isJumping = false;
	}

	model->SetTranslate(newPos);

	return currMoveSpeed != 0;
}

terrain->GetHeight 함수를 통해 현재 서있는 위치의 터레인 높이를 불러와, 플레이어의 높이에 반영하고 있다.

인자로는 x좌표, z좌표를 준 것을 볼 수 있다.

 

다시 말해, GetHeight 함수는 x좌표와 z좌표를 받아, 해당 좌표에 맞는 높이를 반환해주어야 한다.

 

우선 Terrain 클래스에 새로운 멤버로 2차원 벡터 heights를 만들었다.

std::vector<std::vector<GLfloat>> heights;

 

그리고 LoadTerrain 함수에서 height map을 로드하던 것에 더해서 텍셀의 값, 즉 height를 읽어 2차원 벡터에 저장하도록 했다.

void Terrain::LoadTerrain(const char* fileLoc)
{
    // shader setup
    //...
    // -------------------------------------------------------------------------
    // height map 로드
    heightMap = new Texture(fileLoc);
    heightMap->LoadTexture(4, true);
    
    width = heightMap->GetWidth();
    height = heightMap->GetHeight();
    nChannels = heightMap->GetBitDepth();

    unsigned char* heightData = heightMap->GetTexData();

    heights.resize(width);
    for (int i = 0; i < heights.size(); i++)
        heights[i].resize(height);

    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            // retrieve texel for (i,j) tex coord
            unsigned char* texel = heightData + (j + width * i) * nChannels;
            // raw height at coordinate
            GLfloat y = texel[0] / 255.f; // 값의 범위를 [0, 1]로

            heights[j][i] = y; // heights에 값 채워주기
        }
    }

    stbi_image_free(heightData);
    // -------------------------------------------------------------------------
    // diffuse map 로드    
    //...
    // -------------------------------------------------------------------------

    // vertex generation
    //...
}

이렇게 heights 터레인의 높이를 다음과 같이 저장하게 되었다.

 

이제 GetHeight 함수를 올바르게 만들어줘야 하는데,

우리가 Terrain의 버텍스를 GPU로 쏴줄 때, 범위를 [(-width/2, -height/2) , (width/2, height/2)] 로 바꿔주었었다.

즉 터레인의 중심이 (0,0)이 되도록 했었는데, 현재 heights에서 사용할 index의 범위는 [(0,0), (width, height)]이다.

 

따라서 플레이어에게 받은 x좌표, z좌표를 height의 index로 사용할 수 있도록, 다시 값의 범위를 바꿔줄 것이다.

    // worldX, worldZ : 플레이어의 x좌표, z좌표
    float terrainX = worldX + width/2.f;
    float terrainZ = worldZ + height/2.f;

 

그 다음은 플레이어가 서있는 그리드의 네모 칸의 왼쪽 위 점을 알아낼 것이다.

이를 위해서는 소수점을 버려주면 된다.

    int gridX = floorf(terrainX);
    int gridZ = floorf(terrainZ);

    if(gridX >= width-1 || gridZ >= height-1 || gridX < 0 || gridZ < 0)
        return 0.f;

겸사겸사 범위 밖임을 확인했을 경우, out of bounds 에러가 나지 않도록 0을 리턴하도록 하였다.

 

플레이어가 그리드 내부 정확히 어디에 서있는지 알기 위해서는, 반대로 소수점 아래 값을 읽으면 된다.

fmod 함수를 활용하였다.

    float xCoord = fmod(terrainX, gridX);
    float zCoord = fmod(terrainZ, gridZ);

 

 

이제 플레이어가 서있는 그리드의 4개의 꼭짓점의 높이를 모두 읽어준다.

    float h00 = heights[gridX][gridZ];
    float h01 = heights[gridX+1][gridZ];
    float h10 = heights[gridX][gridZ+1];
    float h11 = heights[gridX+1][gridZ+1];

 

그리고 밥먹듯이 해왔던 Bilinear Interpolation을 통해 현재 플레이어가 서있는 지점의 정확한 높이 값을 얻을 수 있다.

    float h0 = glm::mix(h00, h01, xCoord);
    float h1 = glm::mix(h10, h11, xCoord);
    float h = glm::mix(h0, h1, zCoord);

이걸 그림으로 보면 다음과 같다.

 

결과 값 h를 구했지만, 터레인이 scale되고 shift될 수 있도록 만들었으므로, 그것까지 고려하여 최종 리턴 값은 다음과 같다.

    return h * heightScale + heightShift;

 

전체 함수는 다음과 같다.

GLfloat Terrain::GetHeight(float worldX, float worldZ)
{
    float terrainX = worldX + width/2.f;
    float terrainZ = worldZ + height/2.f;

    int gridX = floorf(terrainX);
    int gridZ = floorf(terrainZ);

    if(gridX >= width-1 || gridZ >= height-1 || gridX < 0 || gridZ < 0)
        return 0.f;

    float xCoord = fmod(terrainX, gridX);
    float zCoord = fmod(terrainZ, gridZ);

    float h00 = heights[gridX][gridZ];
    float h01 = heights[gridX+1][gridZ];
    float h10 = heights[gridX][gridZ+1];
    float h11 = heights[gridX+1][gridZ+1];

    float h0 = glm::mix(h00, h01, xCoord);
    float h1 = glm::mix(h10, h11, xCoord);
    float h = glm::mix(h0, h1, zCoord);

    return h * heightScale + heightShift;
}

 

 

전체 소스코드:

https://github.com/sys010611/YsEngine

 

GitHub - sys010611/YsEngine

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

github.com

 

 

실행 결과:

https://www.youtube.com/watch?v=Ro2ALw5RpHs