Có hai điều cốt yếu để chuyển động xuất hiện trơn tru, thứ nhất rõ ràng là những gì bạn kết xuất cần phù hợp với trạng thái mong đợi tại thời điểm khung được hiển thị cho người dùng, thứ hai là bạn cần trình bày khung cho người dùng tại một khoảng thời gian tương đối cố định. Trình bày một khung hình ở T + 10ms, sau đó một khung hình khác tại T + 30ms, sau đó một khung hình khác tại T + 40ms, sẽ xuất hiện cho người dùng đang bị lúng túng, ngay cả khi những gì thực sự hiển thị cho những thời điểm đó là chính xác theo mô phỏng.
Vòng lặp chính của bạn dường như thiếu bất kỳ cơ chế gating nào để đảm bảo rằng bạn chỉ kết xuất theo chu kỳ. Vì vậy, đôi khi bạn có thể thực hiện 3 lần cập nhật giữa các lần kết xuất, đôi khi bạn có thể thực hiện 4. Về cơ bản, vòng lặp của bạn sẽ hiển thị thường xuyên nhất có thể, ngay khi bạn mô phỏng đủ thời gian để đẩy trạng thái mô phỏng trước thời điểm hiện tại, bạn sẽ sau đó kết xuất trạng thái đó. Nhưng bất kỳ sự thay đổi nào trong thời gian cần để cập nhật hoặc kết xuất, và khoảng thời gian giữa các khung cũng sẽ khác nhau. Bạn đã có một dấu thời gian cố định cho mô phỏng của mình, nhưng dấu thời gian thay đổi cho kết xuất của bạn.
Những gì bạn có thể cần là chờ đợi ngay trước khi kết xuất, điều đó đảm bảo rằng bạn chỉ bắt đầu kết xuất khi bắt đầu một khoảng thời gian kết xuất. Lý tưởng nhất là phải thích ứng: nếu bạn mất quá nhiều thời gian để cập nhật / kết xuất và bắt đầu khoảng thời gian đã trôi qua, bạn nên kết xuất ngay lập tức, nhưng cũng tăng thời lượng khoảng thời gian, cho đến khi bạn có thể kết xuất và cập nhật một cách nhất quán kết xuất tiếp theo trước khi khoảng thời gian kết thúc. Nếu bạn có nhiều thời gian rảnh rỗi, thì bạn có thể từ từ giảm khoảng thời gian (tức là tăng tốc độ khung hình) để hiển thị lại nhanh hơn.
Nhưng, và đây là kicker, nếu bạn không kết xuất khung ngay lập tức sau khi phát hiện trạng thái mô phỏng đã được cập nhật thành "ngay bây giờ", thì bạn sẽ giới thiệu răng cưa tạm thời. Khung được trình bày cho người dùng đang được trình bày không đúng lúc, và bản thân nó sẽ cảm thấy như bị nói lắp.
Đây là lý do cho "dấu thời gian một phần" bạn sẽ thấy được đề cập trong các bài viết bạn đã đọc. Đó là một lý do chính đáng, và đó là vì trừ khi bạn sửa dấu thời gian vật lý của mình thành một số tích phân cố định của dấu thời gian kết xuất cố định, bạn chỉ đơn giản là không thể hiển thị các khung vào đúng thời điểm. Bạn kết thúc hoặc trình bày chúng quá sớm, hoặc quá muộn. Cách duy nhất để có được tốc độ kết xuất cố định và vẫn hiển thị thứ gì đó chính xác về mặt vật lý, là chấp nhận rằng tại thời điểm kết xuất xuất hiện, rất có thể bạn sẽ ở giữa hai dấu thời gian vật lý cố định của mình. Nhưng điều đó không có nghĩa là các đối tượng được sửa đổi trong khi kết xuất, chỉ là kết xuất phải tạm thời thiết lập vị trí của các đối tượng để nó có thể hiển thị chúng ở một nơi nào đó ở giữa nơi chúng ở trước và nơi chúng ở sau khi cập nhật. Điều đó quan trọng - không bao giờ thay đổi trạng thái thế giới để kết xuất, chỉ cập nhật mới thay đổi trạng thái thế giới.
Vì vậy, để đặt nó vào một vòng lặp mã giả, tôi nghĩ bạn cần một cái gì đó giống như:
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);
}
Để làm việc này, tất cả các đối tượng đang được cập nhật cần phải bảo tồn kiến thức về nơi chúng ở trước và nơi chúng hiện tại, để kết xuất có thể sử dụng kiến thức về vị trí của đối tượng.
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);
}
}
Và hãy đặt ra dòng thời gian tính bằng mili giây, nói rằng quá trình kết xuất mất 3ms để hoàn thành, quá trình cập nhật mất 1ms, bước thời gian cập nhật của bạn được cố định thành 5ms và dấu thời gian kết xuất của bạn bắt đầu (và duy trì) ở 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
- Đầu tiên chúng tôi khởi tạo tại thời điểm 0 (vì vậy currentTime = 0)
- Chúng tôi kết xuất với tỷ lệ 1.0 (100% currentTime), sẽ thu hút thế giới tại thời điểm 0
- Khi kết thúc, thời gian thực tế là 3 và chúng tôi không hy vọng khung hình sẽ kết thúc đến 16, vì vậy chúng tôi cần chạy một số cập nhật
- T + 3: Chúng tôi cập nhật từ 0 đến 5 (vì vậy sau đó currentTime = 5, trướcTime = 0)
- T + 4: vẫn còn trước khi kết thúc khung, vì vậy chúng tôi cập nhật từ 5 đến 10
- T + 5: vẫn còn trước khi kết thúc khung, vì vậy chúng tôi cập nhật từ 10 đến 15
- T + 6: vẫn còn trước khi kết thúc khung, vì vậy chúng tôi cập nhật từ 15 đến 20
- T + 7: vẫn ở trước đầu khung, nhưng currentTime chỉ vượt ra ngoài đầu khung. Chúng tôi không muốn mô phỏng thêm nữa bởi vì làm như vậy sẽ đẩy chúng tôi vượt quá thời gian tiếp theo chúng tôi muốn kết xuất. Thay vào đó, chúng tôi lặng lẽ chờ khoảng thời gian kết xuất tiếp theo (16)
- T + 16: Đã đến lúc kết xuất lại. trướcTime là 15, currentTime là 20. Vì vậy, nếu chúng ta muốn kết xuất ở T + 16, chúng ta sẽ đi được 1ms trong khoảng thời gian dài 5ms. Vì vậy, chúng tôi là 20% của cách thông qua khung (tỷ lệ = 0,2). Khi chúng ta kết xuất, chúng ta vẽ các đối tượng 20% giữa vị trí trước đó và vị trí hiện tại của chúng.
- Lặp lại 3. và tiếp tục vô thời hạn.
Có một sắc thái khác ở đây về việc mô phỏng quá xa trước thời hạn, có nghĩa là đầu vào của người dùng có thể bị bỏ qua mặc dù chúng đã xảy ra trước khi khung hình thực sự được hiển thị, nhưng đừng lo lắng về điều đó cho đến khi bạn tự tin rằng vòng lặp đang mô phỏng trơn tru.