실제로 보간은 어떻게 물체의 움직임을 부드럽게 하는가?


10

나는 지난 8 개월 동안 비슷한 기쁨을 느끼지 않고 비슷한 질문을했기 때문에 질문을 좀 더 일반적으로 할 것입니다.

OpenGL ES 2.0 인 Android 게임이 있습니다. 그 안에 다음 게임 루프가 있습니다.

내 루프는 고정 시간 단계 원리로 작동합니다 (dt = 1 / ticksPerSecond )

loops=0;

    while(System.currentTimeMillis() > nextGameTick && loops < maxFrameskip){

        updateLogic(dt);
        nextGameTick+=skipTicks;
        timeCorrection += (1000d/ticksPerSecond) % 1;
        nextGameTick+=timeCorrection;
        timeCorrection %=1;
        loops++;

    }

    render();   

내 통합은 다음과 같이 작동합니다.

sprite.posX+=sprite.xVel*dt;
sprite.posXDrawAt=sprite.posX*width;

이제 모든 것이 내가 원하는대로 작동합니다. 2.5 초 안에 물체가 특정 거리 (화면 너비)를 가로 질러 움직 이도록 지정하면됩니다. 또한 게임 루프에서 허용하는 프레임 건너 뛰기 때문에 거의 모든 장치 에서이 작업을 수행 할 수 있으며 항상 2.5 초가 걸립니다.

문제

그러나 렌더링 프레임을 건너 뛸 때 그래픽이 끊기는 문제가 있습니다. 매우 성가신 일입니다. 프레임을 건너 뛰는 기능을 제거하면 원하는대로 모든 것이 매끄럽지 만 다른 장치에서 다른 속도로 실행됩니다. 따라서 옵션이 아닙니다.

나는 왜 프레임이 건너 뛰는 지 확신 할 수 없지만 이것이 성능 저하와 관련없음 을 지적하고 싶습니다 . 코드를 1 개의 작은 스프라이트로 가져 갔으며 논리는 필요하지 않습니다. 스프라이트를 움직여도 여전히 프레임을 건너 뜁니다. 그리고 이것은 Google Nexus 10 태블릿에 있습니다 (위에서 언급했듯이 장치간에 속도를 일관되게 유지하려면 프레임 건너 뛰기가 필요합니다).

따라서 내가 가지고있는 유일한 다른 옵션은 보간 (또는 외삽)을 사용하는 것입니다. 나는 거기에있는 모든 기사를 읽었지만 실제로 어떻게 작동하는지 이해하지 못했고 시도한 모든 구현이 실패했습니다.

한 가지 방법을 사용하면 물건을 부드럽게 움직일 수 있었지만 충돌을 엉망으로 만들 수 없었습니다. 보간이 렌더링 시간에 렌더링 시간에 전달되고 렌더링 시간 내에 보간이 이루어 지므로 유사한 방법으로 동일한 문제를 예상 할 수 있습니다. 충돌 교정하는 위치 (문자 지금 바로 옆에 벽에 서) 그렇다면, 렌더러는 그것의 위치를 변경하고 그릴 수 에서 벽.

정말 혼란 스러워요. 사람들은 당신이해야한다고 말한 적이 렌더링 메소드 내에서 객체의 위치를 변경할 수 없지만, 모든 예제는 온라인이 표시됩니다.

따라서 올바른 방향으로 푸시를 요청하고 있습니다.이 기사를 여러 번 읽었으므로 인기있는 게임 루프 기사 (deWitters, 타임 스텝 수정 등)에 링크하지 마십시오 . 나는 누군가 나를 위해 내 코드를 작성하도록 요구 하지 않습니다 . 보간법이 실제로 몇 가지 예에서 어떻게 작동하는지 간단한 용어로 설명하십시오. 그런 다음 아이디어를 내 코드에 통합하려고 시도하고 더 자세한 요구 사항이 있으면 더 구체적인 질문을 할 것입니다. (이것은 많은 사람들이 어려움을 겪고있는 문제라고 확신합니다).

편집하다

일부 추가 정보-게임 루프에서 사용되는 변수.

private long nextGameTick = System.currentTimeMillis();
//loop counter
private int loops;
//Amount of frames that we will allow app to skip before logic is affected
private final int maxFrameskip = 5;                         
//Game updates per second
final int ticksPerSecond = 60;
//Amount of time each update should take        
private final int skipTicks = (1000 / ticksPerSecond);
float dt = 1f/ticksPerSecond;
private double timeCorrection;

그리고 downvote의 이유는 .....?
BungleBonce

1
때때로 말할 수 없습니다. 이것은 문제를 해결하려고 할 때 좋은 질문이 있어야하는 모든 것 같습니다. 간결한 코드 스 니펫, 시도한 내용에 대한 설명, 연구 시도 및 문제가 무엇인지 알아야 할 사항을 명확하게 설명합니다.
Jesse Dorsey

나는 당신의 공짜가 아니었지만 한 부분을 분명히하십시오. 프레임을 건너 뛰면 그래픽이 끊깁니다. 그것은 명백한 진술처럼 보입니다 (프레임이 누락되었거나 프레임이 누락 된 것처럼 보입니다). 건너 뛰기를 더 잘 설명 할 수 있습니까? 더 이상한 일이 있습니까? 그렇지 않으면 프레임 속도가 떨어지면 부드러운 움직임을 얻을 수 없으므로 해결할 수없는 문제 일 수 있습니다.
Seth Battin

고맙습니다, Noctrine, 사람들이 설명을 남기지 않고 공감할 때 정말로 저를 자극합니다. @SethBattin, 죄송합니다. 물론입니다. 맞습니다. 프레임을 건너 뛰면 멍청이가 발생하지만 위에서 말한 것처럼 일종의 보간 이이를 분류 해야합니다 (그러나 제한적이지만). 내가 틀렸다면 문제는 여러 장치에서 동일한 속도로 원활하게 실행되도록하려면 어떻게 될까요?
BungleBonce

4
해당 문서를주의해서 다시 읽으십시오. 실제로는 렌더링 방법에서 객체의 위치를 ​​수정하지 않습니다. 그들은 단지 지난 시간과 얼마나 많은 시간이 지났는가에 기초하여 현재 위치에 근거하여 방법의 명백한 위치를 수정합니다.
AttackingHobo

답변:


5

모션이 부드럽게 나타나게하는 데 중요한 두 가지 사항이 있습니다. 첫 번째는 렌더링하는 것이 프레임이 사용자에게 제시 될 때 예상 상태와 일치해야한다는 것이고, 두 번째는 사용자에게 프레임을 제시해야한다는 것입니다. 비교적 고정 된 간격으로. T + 10ms에서 프레임을 제시 한 다음 T + 30ms에서 다른 프레임을 제시 한 다음 T + 40ms에서 다른 프레임을 제시하면 시뮬레이션에 따라 해당 시간 동안 실제로 표시된 내용이 정확하더라도 사용자가 판단하는 것처럼 보입니다.

메인 루프에는 일정한 간격으로 만 렌더링 할 수있는 게이팅 메커니즘이없는 것 같습니다. 때로는 렌더링간에 3 번의 업데이트를 수행하고 때로는 4 번을 수행 할 수도 있습니다. 기본적으로 현재 시간 앞에서 시뮬레이션 상태를 푸시하기에 충분한 시간을 시뮬레이션하면 루프가 최대한 자주 렌더링됩니다. 그 상태를 렌더링합니다. 그러나 업데이트 또는 렌더링에 걸리는 시간과 프레임 간 간격도 다양합니다. 시뮬레이션에는 정해진 시간 간격이 있지만 렌더링에는 다양한 시간 간격이 있습니다.

렌더링 직전의 대기 시간이 필요할 수 있습니다. 이렇게하면 렌더링 간격이 시작될 때만 렌더링을 시작할 수 있습니다. 이상적으로는 적응해야합니다. 업데이트 / 렌더링에 너무 오래 걸리고 간격의 시작이 이미 지났다면 즉시 렌더링해야하지만 일관성있게 렌더링 및 업데이트 할 수있을 때까지 인터벌 길이를 늘려야합니다. 간격이 끝나기 전에 다음 렌더 여유 시간이 충분하면 간격을 천천히 줄이고 (즉, 프레임 속도를 높이면) 다시 빠르게 렌더링 할 수 있습니다.

그러나 여기에 키커가 있습니다. 시뮬레이션 상태가 "지금"으로 업데이트 된 것을 감지 한 직후에 프레임을 렌더링하지 않으면 임시 앨리어싱이 도입됩니다. 사용자에게 제공되는 프레임이 약간 잘못된 시간에 표시되고 있으며, 그 자체가 더듬 거리는 느낌이들 것입니다.

이것이 당신이 읽은 기사에서 언급 된 "부분 시간 단계"의 이유입니다. 거기에는 합당한 이유가 있습니다. 물리적 타임 스텝을 고정 렌더링 타임 스텝의 고정 적분 배수로 고정하지 않으면 적시에 프레임을 표시 할 수 없기 때문입니다. 너무 일찍 또는 너무 늦게 발표 할 수 있습니다. 고정 렌더링 속도를 얻고 물리적으로 올바른 것을 제시하는 유일한 방법은 렌더링 간격이 다가올 때 고정 물리 시간 단계 중 두 단계 사이에있을 가능성이 있다는 점을 받아들이는 것입니다. 그렇다고 렌더링 중에 객체가 수정되는 것은 아닙니다. 렌더링은 오브젝트가있는 위치를 임시로 설정하여 오브젝트가 업데이트 이전과 이전의 위치 사이에 렌더링되도록 할 수 있습니다. 중요합니다. 렌더링을 위해 월드 상태를 변경하지 말고 업데이트 만 월드 상태를 변경해야합니다.

의사 코드 루프에 넣으려면 다음과 같은 것이 더 필요하다고 생각합니다.

InitialiseWorldState();

previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval

subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame

while (true)
{
    frameStart = ActualTime();

    //Render the world state as if it was some proportion 
    // between previousTime and currentTime
    // E.g. if subFrameProportion is 0.5, previousTime is 0.1 and 
    // currentTime is 0.2, then we actually want to render the state
    // as it would be at time 0.15. We'd do that by interpolating 
    // between movingObject.previousPosition and movingObject.currentPosition
    // with a lerp parameter of 0.5
    Render(subFrameProportion); 

    //Check we've not taken too long and missed our render interval
    frameTime = ActualTime() - frameStart;
    if (frameTime > renderInterval)
    {
        renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
    }

    expectedFrameEnd = frameStart + renderInterval;

    //Loop until it's time to render the next frame
    while (ActualTime() < expectedFrameEnd)
    {
        //step the simulation forward until it has moved just beyond the frame end
        if (previousTime < expectedFrameEnd) &&
            currentTime >= expectedFrameEnd)
        {
            previousTime = currentTime;

            Update();
            currentTime += fixedTimeStep;

            //After the update, all objects will be in the position they should be for
            // currentTime, **but** they also need to remember where they were before,
            // so that the rendering can draw them somewhere between previousTime and
            //  currentTime

            //Check again we've not taken too long and missed our render interval
            frameTime = ActualTime() - frameStart;
            if (frameTime > renderInterval)
            {
                renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
                expectedFrameEnd = frameStart + renderInterval
            }
        }
        else
        {
            //We've brought the simulation to just after the next time
            // we expect to render, so we just want to wait.
            // Ideally sleep or spin in a tight loop while waiting.
            timeTillFrameEnd = expectedFrameEnd - ActualTime();
            sleep(timeTillFrameEnd);
        }
    }

    //How far between update timesteps (i.e. previousTime and currentTime)
    // will we be at the end of the frame when we start the next render?
    subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}

이를 위해 업데이트되는 모든 객체가 작동하려면 렌더링이 객체의 위치에 대한 지식을 사용할 수 있도록 이전 및 현재 위치에 대한 지식을 보존해야합니다.

class MovingObject
{
    Vector velocity;
    Vector previousPosition;
    Vector currentPosition;

    Initialise(startPosition, startVelocity)
    {
        currentPosition = startPosition; // position at time 0
        velocity = startVelocity;
        //ignore previousPosition because we should never render before time 0
    }

    Update()
    {
        previousPosition = currentPosition;
        currentPosition += velocity * fixedTimeStep;
    }

    Render(subFrameProportion)
    {
        Vector actualPosition = 
            Lerp(previousPosition, currentPosition, subFrameProportion);
        RenderAt(actualPosition);
    }
}

렌더링 시간은 3ms, 업데이트 시간은 1ms, 업데이트 시간 단계는 5ms로, 렌더링 시간 단계는 16ms [60Hz]에서 시작 (및 유지)된다는 타임 라인을 밀리 초 단위로 표시해 보겠습니다.

0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33
R0          U5  U10 U15 U20 W16                                 R16         U25 U30 U35 W32                                 R32
  1. 먼저 우리는 시간 0에서 초기화합니다 (따라서 currentTime = 0)
  2. 1.0 (100 % currentTime)의 비율로 렌더링하여 시간 0에 세계를 그립니다.
  3. 완료되면 실제 시간은 3이며 프레임이 16까지 끝날 것으로 예상하지 않으므로 업데이트를 실행해야합니다.
  4. T + 3 : 0에서 5로 업데이트합니다 (따라서 currentTime = 5, previousTime = 0)
  5. T + 4 : 프레임이 끝나기 전에 5에서 10으로 업데이트
  6. T + 5 : 여전히 프레임이 끝나기 전에 10에서 15로 업데이트
  7. T + 6 : 프레임이 끝나기 전에 15에서 20으로 업데이트
  8. T + 7 : 여전히 프레임 끝 이전이지만 currentTime은 프레임 끝을 초월합니다. 더 이상 시뮬레이션하고 싶지 않다면 다음 렌더링 시간을 넘어서게됩니다. 대신 다음 렌더링 간격을 조용히 기다립니다 (16).
  9. T + 16 : 다시 렌더링해야합니다. previousTime은 15, currentTime은 20입니다. 따라서 T + 16에서 렌더링하려면 5ms의 긴 시간 간격으로 1ms가됩니다. 따라서 우리는 프레임을 통과하는 길이의 20 %입니다 (비율 = 0.2). 렌더링 할 때 이전 위치와 현재 위치 사이의 20 %의 오브젝트를 그립니다.
  10. 다시 3으로 루프하고 무한정 계속하십시오.

너무 미리 시뮬레이션하는 것에 대한 또 다른 뉘앙스가 있습니다. 즉, 프레임이 실제로 렌더링되기 전에 발생하더라도 사용자의 입력이 무시 될 수 있지만 루프가 원활하게 시뮬레이션된다고 확신 할 때까지 걱정하지 마십시오.


주의 : 의사 코드는 두 가지 방식으로 약합니다. 첫째, 그것은 죽음의 나선형 사례를 포착하지 못합니다 (fixedTimeStep to Update보다 시간이 오래 걸리므로 시뮬레이션이 훨씬 뒤쳐져 사실상 무한 루프입니다). 둘째로 renderInterval이 다시 단축되지 않습니다. 실제로 renderInterval을 즉시 늘리고 싶지만 시간이 지남에 따라 실제 프레임 시간의 허용 오차 내에서 최대한 최대한 점차적으로 줄입니다. 그렇지 않으면 하나의 불량 / 장시간 업데이트가 낮은 프레임 속도로 영원히 안장됩니다.
MrCranky 2014 년

이 @MrCranky에 감사드립니다. 실제로, 루프에서 렌더링을 '제한'하는 방법에 대해 오랫동안 고민하고 있습니다! 그것을하는 방법을 해결할 수 없었고 그것이 그 문제 중 하나 일지 궁금해했습니다. 나는 이것에 대해 제대로 읽고 당신의 제안을 시도하고 다시보고 할 것입니다! 다시 한 번 감사드립니다 :-)
BungleBonce

감사합니다 @ MrCranky, OK, 나는 당신의 대답을 읽고 다시 읽었지만 이해할 수 없습니다 :-( 구현하려고했지만 빈 화면이 생겼습니다. 이것으로 정말로 고생하고 있습니다. 움직이는 객체의 이전 및 현재 위치와 관련이 있습니까? 또한 "currentFrame = Update ();"줄은 어떻습니까?-이 줄을 얻지 못하면 update ()를 호출합니까? 그렇지 않으면 update라고 부르거나 currentFrame (포지션)을 새로운 값으로 설정한다는 의미입니까? 도움을 주셔서 다시 한 번 감사드립니다 !!
BungleBonce

예, 효과적으로 이전 프레임과 currentFrame을 Update 및 InitialiseWorldState의 반환 값으로 넣는 이유는 고정 된 두 업데이트 단계 사이에서 렌더링이 월드를 그릴 수 있도록하기 위해 모든 위치의 현재 위치 만 가질 필요는 없기 때문입니다. 그리려는 객체뿐만 아니라 이전 위치. 각 객체가 두 값을 내부적으로 저장하도록함으로써 다루기가 어려워집니다.
MrCranky

그러나 시간 T에서 세계의 현재 상태를 나타내는 데 필요한 모든 상태 정보가 단일 객체 아래 유지되도록 사물을 설계하는 것도 가능하지만 훨씬 어렵습니다. 개념적으로 프레임 상태를 업데이트 단계에서 생성 된 것으로 처리 할 수 ​​있으므로 시스템에서 어떤 정보가 있는지 설명 할 때 훨씬 더 깔끔하며 이전 프레임을 유지하는 것은 해당 프레임 상태 객체 중 하나 이상을 유지하는 것입니다. 그러나 실제로 구현했을 때와 조금 더 비슷하게 답변을 다시 작성할 수 있습니다.
MrCranky

3

모두가 당신에게 말한 것은 맞습니다. 렌더링 로직에서 스프라이트의 시뮬레이션 위치를 업데이트하지 마십시오.

이렇게 생각하면 스프라이트에는 2 가지 위치가 있습니다. 시뮬레이션이 마지막 시뮬레이션 업데이트 시점이며 스프라이트가 렌더링되는 위치입니다. 그들은 완전히 다른 두 좌표입니다.

스프라이트는 외삽 된 위치에 렌더링됩니다. 외삽 된 위치는 각 렌더 프레임을 계산하여 스프라이트를 렌더하는 데 사용됩니다. 그것이 전부입니다.

그 외에는 잘 이해하고있는 것 같습니다. 도움이 되었기를 바랍니다.


우수한 @WilliamMorrison-이것을 확인해 주셔서 감사합니다, 나는 이것이 사실이라고 결코 100 % 확신하지 못했습니다.
BungleBonce

궁금한 @WilliamMorrison은 이러한 버림 좌표를 사용하여 스프라이트가 다른 객체에 '내장'되거나 '바로 위에'그려지는 문제를 어떻게 완화 할 수 있을까요? 렌더링 시간에도 충돌 코드를 실행해야합니까?
BungleBonce

내 게임에서 그렇습니다. 나보다 나아주세요, 그렇게하지 마십시오, 그것은 최고의 솔루션이 아닙니다. 사용하지 말아야 할 로직으로 렌더링 코드를 복잡하게 만들고 중복 충돌 감지시 CPU를 낭비합니다. 두 번째 위치에서 마지막 위치와 현재 위치 사이를 보간하는 것이 좋습니다. 이렇게하면 잘못된 위치에 외삽하지 않아도 문제가 해결되지만 시뮬레이션 뒤에서 한 단계 렌더링 할 때 문제가 복잡해집니다. 당신의 의견, 당신이 취하는 접근법, 그리고 경험을 듣고 싶습니다.
윌리엄 모리슨

그래, 해결하기 까다로운 문제입니다. 나는 이것에 대해 별도의 질문을 했었습니다 .gamedev.stackexchange.com / questions / 83230 /… 당신이 그것을 주시하거나 무언가를 기여하고 싶다면. 자, 당신이 당신의 의견에서 제안한 것은 이미 이것을하고 있지 않습니까? (이전 프레임과 현재 프레임 사이의 보간)?
BungleBonce

좀 빠지는. 당신은 실제로 지금 외삽하고 있습니다. 시뮬레이션에서 최신 데이터를 가져 와서 소수의 시간 단계 후에 해당 데이터가 어떻게 보이는지 추정합니다. 마지막 시뮬레이션 위치와 현재 시뮬레이션 위치 사이를 분수 시간 단계로 보간하여 렌더링하는 것이 좋습니다. 렌더링은 시뮬레이션보다 1 배 늦습니다. 이 보장하지만 당신은 시뮬레이션을 확인하지 않은 상태에서 개체를 렌더링 않을거야 (. 시뮬레이션이 실패하지 않는 즉 발사체가 벽에 표시되지 않습니다.)
윌리엄 모리슨에게
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.