गति के सुचारू रूप से प्राप्त होने के लिए दो चीजें महत्वपूर्ण हैं, पहला यह है कि स्पष्ट रूप से आपको उस समय अपेक्षित स्थिति से मिलान करने की आवश्यकता है जिस समय फ्रेम उपयोगकर्ता को प्रस्तुत किया जाता है, दूसरा यह है कि आपको उपयोगकर्ता को फ्रेम प्रस्तुत करने की आवश्यकता है अपेक्षाकृत निश्चित अंतराल पर। 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
- पहले हम समय पर आरंभ करते हैं 0 (इसलिए वर्तमान समय = 0)
- हम 1.0 (100% वर्तमान समय) के अनुपात के साथ प्रस्तुत करते हैं, जो दुनिया को 0 पर आकर्षित करेगा
- जब वह पूरा हो जाता है, तो वास्तविक समय 3 है, और हम फ्रेम को 16 तक समाप्त होने की उम्मीद नहीं करते हैं, इसलिए हमें कुछ अपडेट चलाने की आवश्यकता है
- T + 3: हम 0 से 5 तक अपडेट करते हैं (इसलिए बाद में currentTime = 5, पिछले समय = 0)
- T + 4: अभी भी फ्रेम समाप्त होने से पहले, इसलिए हम 5 से 10 तक अपडेट करते हैं
- T + 5: अभी भी फ्रेम समाप्त होने से पहले, इसलिए हम 10 से 15 तक अपडेट करते हैं
- T + 6: अभी भी फ्रेम समाप्त होने से पहले, इसलिए हम 15 से 20 तक अपडेट करते हैं
- T + 7: अभी भी फ्रेम समाप्त होने से पहले, लेकिन वर्तमान समय सीमा के अंत से परे है। हम आगे कोई अनुकरण नहीं करना चाहते क्योंकि ऐसा करने से हमें उस समय से परे धकेल दिया जाएगा जब हम अगली बार प्रस्तुत करना चाहते हैं। इसके बजाय हम अगले रेंडर अंतराल (16) के लिए चुपचाप प्रतीक्षा करते हैं
- T + 16: यह फिर से प्रस्तुत करने का समय है। पिछला समय 15 है, वर्तमान समय 20 है। इसलिए यदि हम T + 16 पर रेंडर करना चाहते हैं, तो हम 5ms लंबे टाइमस्टेप के माध्यम से 1ms हैं। तो हम फ्रेम (अनुपात = 0.2) के माध्यम से 20% रास्ते हैं। जब हम रेंडर करते हैं, तो हम वस्तुओं को उनकी पिछली स्थिति और उनकी वर्तमान स्थिति के बीच 20% का रास्ता बनाते हैं।
- 3. वापस लूप करें और अनिश्चित काल तक जारी रखें।
समय से बहुत आगे अनुकरण करने के बारे में यहां एक और बारीकियों है, जिसका अर्थ है कि उपयोगकर्ता के इनपुट को अनदेखा किया जा सकता है, भले ही वे फ्रेम वास्तव में प्रदान किए जाने से पहले हुए थे, लेकिन इसके बारे में चिंता न करें जब तक आप आश्वस्त न हों कि लूप आसानी से अनुकरण कर रहा है।