엔진 개발 - 1

46374 단어 유체유체

1 기초

1.1. 안녕, 유체 시뮬레이션아?

이 예시의 목표: 아래 그림처럼 두 파동을 1차원 세상에서 표현하기. 파동이 끝에 닿으면 반대 방향으로 진행하도록.

1.1.1. 상태 정의

파동을 코드로 표현하는 법. 주어진 시간에 파동은 어떤 위치에 어떤 속도값을 가질 것. 또한 모향 또한 파동의 위치에 따라 결정 가능. 그러므로 파동의 상태란 위치와 속도 두 개로 결정 가능.

int main()
{
    double x = 0.0;
    double y = 1.0;
    double speedX = 1.0;
    double speedY = -0.5;
    
    return 0;
}

알파벳 X와 Y가 두 파동. X는 맨 왼쪽에서, Y는 맨 오른쪽에서 출발. X가 Y의 속도 두 배.

네 개의 변수로 시뮬레이션의 "상태" 정의. 이게 시뮬레이션 엔진 설계할 때 제일 중요한 단계. 복잡할 수록 여러 자료 구조 사용하므로 시뮬레이션 동안 추적할 양을 알고, 어떤 자료 구조를 사용할지를 결정해야. 저장할 자료 모델 결정한 다음 단계는 숨을 불어 넣는 단계.

1.1.2. 움직임 연산

움직이려면 "시간" 정의해야 함:

constexpr const int FPS = 100;
constexpr const double TIME_INTERVAL = 1.0 / FPS;

int main()
{
    double x = 0.0;
    double y = 1.0;
    double speedX = 1.0;
    double speedY = -0.5;
    
    for (int i = 0; i < 1'000; ++i)
    {
    	// 파동 갱신
    }
    
    return 0;
}

FPS란 "초당 프레임(FPS)". 초당 그릴 프레임의 수를 정의. 역수가 프레임 당 초. 이게 두 프레임 사이의 시간 간격. TIME_INTERVAL에 0.01초.

1,000 번 도는 반복문으로 파동의 실제 움직임 처리. 처리할 함수 정의:

void UpdateWave(const double timeInterval, double& outX, double& outSpeed)
{
    outX += timeInterval * outSpeed;
}

시간 간격과 현재 위치를 통해 현재 위치를 갱신. 그 정도는 얼마나 빨리 움직이는지(outSpeed)와 얼마나 오래 움직였는지(timeInterval)에 달림. 방향도 outSpeed에 의해 결정.

누적 혹은 적분으로 상태 갱신. 함수 호출 당 적분량이 물리량의 변화량 곱하기 시간 간격. 이런 방식으로 컴퓨터에서 미분방정식 해결하는 가장 간단한 방법.

1.1.3. 경계 처리

벽에 부딪히면 반대 방향으로 가게 처리:

void UpdateWave(const double timeInterval, double& outX, double& outSpeed)
{
    outX += timeInterval * outSpeed;
    
    // 경계 반사
    if (outX > 1.0)
    {
    	outSpeed *= -1.0;
        outX = 1.0 + timeInterval * outSpeed;
    }
    else if (x < 0.0)
    {
    	outSpeed *= -1.0
        outX = timeInterval * outSpeed;
    }
}

위치 갱신한 후 벽에 부딪혔으면 속도의 부호 바꾼 다음, 그곳에서부터 새 위치 계산. 가장 간단한 방법. 이제 UpdateWave 함수를 메인 반복문에서 호출:

constexpr const int FPS = 100;
constexpr const double TIME_INTERVAL = 1.0 / FPS;

void UpdateWave(const double timeInterval, double& outX, double& outSpeed)
{
    outX += timeInterval * outSpeed;
    
    // 경계 반사
    if (outX > 1.0)
    {
    	outSpeed *= -1.0;
        outX = 1.0 + timeInterval * outSpeed;
    }
    else if (x < 0.0)
    {
    	outSpeed *= -1.0
        outX = timeInterval * outSpeed;
    }
}

int main()
{
    double x = 0.0;
    double y = 1.0;
    double speedX = 1.0;
    double speedY = -0.5;
    
    for (int i = 0; i < 1'000; ++i)
    {
    	// 파동 갱신
        UpdateWave(TIME_INTERVAL, x, speedX);
        UpdateWave(TIME_INTERVAL, y, speedY);
    }
    
    return 0;
}

1.1.4. 시각화

다른 거 없이 터미널에서 시각화:

#include <array>
#include <cstdio>

constexpr const size_t BUFFER_SIZE = 80;
constexpr const int FPS = 100;
constexpr const double TIME_INTERVAL = 1.0 / FPS;

void UpdateWave(const double timeInterval, double& outX, double& outSpeed);

int main()
{
    const double waveLengthX = 0.8;
    const double waveLengthY = 1.2;
    
    const double maxHeightX = 0.5;
    const double maxHeightY = 0.4;
    
    double x = 0.0;
    double y = 1.0;
    double speedX = 1.0;
    double speedY = -0.5;
    
    std::array<double, BUFFER_SIZE> heightField;
    
    for (int i = 0; i < 1'000; ++i)
    {
    	// 파동 갱신
        UpdateWave(TIME_INTERVAL, x, speedX);
        UpdateWave(TIME_INTERVAL, y, speedY);
    }
    
    return 0;
}

waveLengthX, waveLengthY, maxHeightX, maxHeightY 네 변수가 파동의 형태 결정. heightField는 아래 그림에서처럼 각 0, 1, …, N - 1 번째 원소가 곧 0.5/N, 1.5/N, …, N - 0.5/N 위치에서의 파동의 높이값.

파동이 코사인 형태를 띤다고 가정. 그러면 애니메이션은 아래 그림과 같은 경우들이 있을 것:

이제 그리기 위해 함수 추가:

#include <cmath>

...

void AccumulateWaveToHeightField(const double x,
				 const double waveLength,
                 		 const double maxHeight,
                         	 std::array<double, BUFFER_SIZE>& outHeightField)
{
    const double quarterWaveLength = 0.25 * waveLength;
    const int start = static_cast<int>((x - quarterWaveLength) * BUFFER_SIZE);
    const int end = static_cast<int>((x + quarterWaveLength) * BUFFER_SIZE);
    
    for (int i = start; i < end; ++i)
    {
    	int newI = i;
        if (i < 0)
        {
            newI = -i - 1;
        }
        else if (i >= static_cast<int>(BUFFER_SIZE))
        {
            newI = 2 * BUFFER_SIZE - i - 1;
        }
        
        double distance = fabs((i + 0.5) / BUFFER_SIZE - x);
        double height = maxHeight * 0.5 
        		* (cos(min(distance * M_PI / quarterWaveLength, M_PI)) + 1.0);
        outHeightField[newI] += height;
    }
}

클램핑한 코사인 함수를 입력받은 높이 체에 누적시킴. 다음과 같이 최종 코드 정리 가능:

#include <array>
#include <cmath>
#include <cstdio>

constexpr const size_t BUFFER_SIZE = 80;
constexpr const int FPS = 100;
constexpr const double TIME_INTERVAL = 1.0 / FPS;

void AccumulateWaveToHeightField(const double x,
				 const double waveLength,
                 		 const double maxHeight,
                         	 std::array<double, BUFFER_SIZE>& outHeightField);
void UpdateWave(const double timeInterval, double& outX, double& outSpeed);

int main()
{
    const double waveLengthX = 0.8;
    const double waveLengthY = 1.2;
    
    const double maxHeightX = 0.5;
    const double maxHeightY = 0.4;
    
    double x = 0.0;
    double y = 1.0;
    double speedX = 1.0;
    double speedY = -0.5;
    
    std::array<double, BUFFER_SIZE> heightField;
    
    for (int i = 0; i < 1'000; ++i)
    {
    	// 파동 갱신
        UpdateWave(TIME_INTERVAL, x, speedX);
        UpdateWave(TIME_INTERVAL, y, speedY);
        
        // 높이 체 초기화
        for (double& height : heightField)
        {
            height = 0.0;
        }
        
        // 강 중심점마다 파동 누적
        AccumulateWaveToHeightField(x, waveLengthX, maxHeightX, heightField);
        AccumulateWaveToHeightField(y, waveLengthY, maxHeightY, heightField);
    }
    
    return 0;
}

이제 높이 체의 파동 점을 시각화해주면 됨. 비트맵 화면에 점들을 래스터라이징해주는 것의 1차원 버전. 간단한 ASCII 속임수로 터미널에 출력:

#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <string>
#include <thread>

constexpr const size_t BUFFER_SIZE = 80;
constexpr const int FPS = 100;
constexpr const char GRAY_SCALE_TABLE[] = " .:-=+*#%@";
constexpr const size_t GRAY_SCALE_TABLE_SIZE = sizeof(GRAY_SCALE_TABLE) / sizeof(char);
constexpr const double TIME_INTERVAL = 1.0 / FPS;

void AccumulateWaveToHeightField(const double x,
				 const double waveLength,
                 		 const double maxHeight,
                         	 std::array<double, BUFFER_SIZE>& outHeightField);

void Draw(const std::array<double, BUFFER_SIZE>& heightField)
{
    std::string buffer(BUFFER_SIZE, ' ');
    
    // 높이 체를 그레이 스케일로 변환
    for (size_t i = 0; i < BUFFER_SIZE; ++i)
    {
    	double height = heightField[i];
        size_t tableIndex = min(static_cast<size_t>(floor(GRAY_SCALE_TABLE_SIZE * height)), GRAY_SCALE_TABLE_SIZE - 1);
        buffer[i] = GRAY_SCALE_TABLE[tableIndex];
    }
    
    // 기존 출력 지우기
    for (size_t i = 0; i < BUFFER_SIZE; ++i)
    {
    	printf("\b");
    }
    
    // 새 버퍼 출력
    printf("%s", buffer.c_str());
    fflush(stdout);
}

void UpdateWave(const double timeInterval, double& outX, double& outSpeed);

int main()
{
    const double waveLengthX = 0.8;
    const double waveLengthY = 1.2;
    
    const double maxHeightX = 0.5;
    const double maxHeightY = 0.4;
    
    double x = 0.0;
    double y = 1.0;
    double speedX = 1.0;
    double speedY = -0.5;
    
    std::array<double, BUFFER_SIZE> heightField;
    
    for (int i = 0; i < 1'000; ++i)
    {
    	// 파동 갱신
        UpdateWave(TIME_INTERVAL, x, speedX);
        UpdateWave(TIME_INTERVAL, y, speedY);
        
        // 높이 체 초기화
        for (double& height : heightField)
        {
            height = 0.0;
        }
        
        // 강 중심점마다 파동 누적
        AccumulateWaveToHeightField(x, waveLengthX, maxHeightX, heightField);
        AccumulateWaveToHeightField(y, waveLengthY, maxHeightY, heightField);
        
        // 높이 체 그리기
        Draw(heightField);
        
        // 대기
        std::this_thread::sleep_for(std::chrono::milliseconds(1'000 / FPS));
    }
    
    printf("\n");
    fflush(stdout);
    
    return 0;
}

1.1.5. 최종 결과

유체 엔진 개발할 때의 핵심 아이디어들을 배우기 위한 과정. 시뮬레이션 상태 정의, 시간에 따른 상태 갱신, 비유체 개체와의 상호작용 처리, 결과 시각화.

좋은 웹페이지 즐겨찾기