फेजर में एक और छाया विजेता, या साइकिल का उपयोग

दो साल पहले, मैं पहले से ही फेजर 2 डी में छाया पदार्थों के साथ प्रयोग कर रहा था। आखिरी लुडम डेयर में, हमने अचानक एक डरावनी बनाने का फैसला किया, और छाया और रोशनी के बिना एक डरावनी क्या! मैंने अपने पोर पोर ...

... और एलडी के लिए समय में लानत नहीं। खेल में, ज़ाहिर है, थोड़ी रोशनी और छाया है, लेकिन यह वास्तव में क्या होना चाहिए, इसका एक दुखी सादृश्य है।

खेल को प्रतियोगिता में भेजने के बाद घर लौटते हुए, मैंने निर्णय लिया कि "जेस्टाल्ट को बंद करें" और इन दुर्भाग्यपूर्ण छायाओं को समाप्त करें। क्या हुआ - आप खेल में महसूस कर सकते हैं, डेमो में खेल सकते हैं , चित्र देख सकते हैं और लेख में पढ़ सकते हैं।

हमेशा की तरह ऐसे मामलों में, सामान्य समाधान लिखने की कोशिश करने का कोई मतलब नहीं है, आपको एक विशिष्ट स्थिति पर ध्यान केंद्रित करने की आवश्यकता है। खेल की दुनिया को सेगमेंट के रूप में दर्शाया जा सकता है - कम से कम उन संस्थाओं को जो छाया डालते हैं। दीवारें आयताकार हैं, लोग आयताकार हैं, केवल घुमाए गए हैं, हीन बिगाड़ने वाला एक चक्र है, लेकिन कट-ऑफ मॉडल में इसे एक व्यास की लंबाई तक सरलीकृत किया जा सकता है जो प्रकाश की किरण के लिए हमेशा लंबवत होता है।

कई प्रकाश स्रोत (20-30) हैं, और उनमें से सभी परिपत्र (स्पॉटलाइट) हैं और प्रबुद्ध वस्तुओं की तुलना में सशर्त रूप से कम स्थित हैं (ताकि छाया अनंत हो सके)।

मैंने अपने सिर में समस्या को हल करने के निम्नलिखित तरीके देखे:

  1. प्रत्येक प्रकाश स्रोत के लिए, हम एक स्क्रीन (अच्छी तरह से, या 2-4 गुना छोटे) के आकार की बनावट का निर्माण करते हैं। इस बनावट पर, हम बस ट्रेपोजॉइड BCC'D को आकर्षित करते हैं, जहां A प्रकाश स्रोत है, BC रेखा खंड है, B'C 'बनावट के किनारे पर रेखा का प्रक्षेपण है। उसके बाद, इन बनावटों को छायादार को भेजा जाता है, जहां उन्हें एक ही तस्वीर में मिलाया जाता है।

    सेलेस्टे प्लेटफ़ॉर्मर के लेखक ने कुछ ऐसा किया, जो उनके लेख में मध्यम पर अच्छी तरह से लिखा है: मध्यम. com / @NoelFB / remaking-celestes-lighting-3478d6f10bf

    समस्याएं: 20-30 स्क्रीन-आकार के बनावट जिन्हें लगभग हर फ्रेम को फिर से तैयार करने और GPU में लोड करने की आवश्यकता होती है। मुझे याद है कि यह एक बहुत, बहुत तेज प्रक्रिया नहीं थी।

  2. हाब पर पोस्ट में वर्णित विधि - habr.com/post/272233 प्रत्येक प्रकाश स्रोत के लिए हम एक "गहराई का नक्शा" बनाते हैं, अर्थात ऐसी बनावट, जहां x = प्रकाश स्रोत से "बीम" का कोण, y = प्रकाश स्रोत की संख्या, और रंग == स्रोत से निकटतम बाधा की दूरी। यदि हम 0.7 डिग्री (360/512), और 32 प्रकाश स्रोतों का एक कदम उठाते हैं, तो हमें एक 512x32 बनावट मिलती है, जो इतने लंबे समय तक अपडेट नहीं हुई है।
    (45 डिग्री के एक चरण के लिए उदाहरण बनावट)
  3. गुप्त तरीका जो मैं बहुत अंत में बताऊंगा

अंत में, मैं विधि 2 पर आ गया। हालाँकि, लेख में वर्णित वर्णन मुझे अंत तक पसंद नहीं आया। वहां, बनावट को राॅडकास्ट का उपयोग करके शेडर में भी बनाया गया था - चक्र में शेडर प्रकाश स्रोत से बीम की दिशा में चला गया और एक बाधा की तलाश में था। अपने पिछले प्रयोगों में, मैंने शेडर में रैकास्ट भी बनाया था, और यह बहुत महंगा था, यद्यपि सार्वभौमिक था।

"हमारे पास मॉडल में केवल खंड हैं," मैंने सोचा, "और 10-20 खंड किसी भी प्रकाश स्रोत के दायरे में आते हैं। क्या मैं इस पर आधारित दूरी के नक्शे की जल्दी से गणना नहीं कर सकता? "

इसलिए मैंने ऐसा करने का फैसला किया।

इसके साथ शुरू करने के लिए, मैंने बस दीवारों पर, सशर्त "मुख्य चरित्र" और प्रकाश स्रोतों को प्रदर्शित किया। प्रकाश स्रोतों के आसपास, अंधेरे में शुद्ध स्पष्ट प्रकाश का एक चक्र कट जाता है। इसे पाने के लिए:

( डेमो )

मैंने तुरंत ही शेडर के साथ किस करना शुरू कर दिया ताकि आराम ना मिले। प्रत्येक प्रकाश स्रोत के लिए इसे पारित करना आवश्यक था इसके निर्देशांक और कार्रवाई की त्रिज्या (जिसके आगे प्रकाश नहीं पहुंचता है), यह एक समान सरणी के माध्यम से किया जाता है। और फिर शेडर में (जो टुकड़ा है, जो स्क्रीन पर प्रत्येक पिक्सेल के लिए किया जाता है), यह समझना जारी रहा कि वर्तमान पिक्सेल हमारे प्रबुद्ध सर्कल में है या नहीं।
class SimpleLightShader extends Phaser.Filter { constructor(game) { super(game); let lightsArray = new Array(MAX_LIGHTS*4); lightsArray.fill(0, 0, lightsArray.length); this.uniforms.lightsCount = {type: '1i', value: 0}; this.uniforms.lights = {type: '4fv', value: lightsArray}; this.fragmentSrc = ` precision highp float; uniform int lightsCount; uniform vec4 lights[${MAX_LIGHTS}]; void main() { float lightness = 0.; for (int i = 0; i < ${MAX_LIGHTS}; i++) { if (i >= lightsCount) break; vec4 light = lights[i]; lightness += step(length(light.xy - gl_FragCoord.xy), light.z); } lightness = clamp(0., 1., lightness); gl_FragColor = mix(vec4(0,0,0,0.5), vec4(0,0,0,0), lightness); } `; } updateLights(lightSources) { this.uniforms.lightsCount.value = lightSources.length; let i = 0; let array = this.uniforms.lights.value; for (let light of lightSources) { array[i++] = light.x; array[i++] = game.world.height - light.y; array[i++] = light.radius; i++; } } } 

अब हमें प्रत्येक प्रकाश स्रोत के लिए समझने की जरूरत है कि कौन सा सेगमेंट एक छाया डालेगा। बल्कि, सेगमेंट के किन हिस्सों में - नीचे दिए गए चित्र में हम सेगमेंट के "लाल" हिस्सों में रुचि नहीं रखते हैं, क्योंकि प्रकाश अभी भी उन तक नहीं पहुंचता है।

नोट: प्रतिच्छेदन परिभाषा प्रारंभिक अनुकूलन का एक प्रकार है। प्रकाश स्रोत की त्रिज्या से परे खंडों के बड़े टुकड़ों को नष्ट करते हुए, आगे की प्रक्रिया के समय को कम करने के लिए इसकी आवश्यकता है। यह तब समझ में आता है जब हमारे पास कई खंड होते हैं जिनकी लंबाई "चमक" की त्रिज्या से बहुत अधिक होती है। यदि यह मामला नहीं है, और हमारे पास कई छोटे खंड हैं, तो यह सही हो सकता है कि चौराहे का समय बर्बाद न करें और पूरे खंडों को संसाधित करें, क्योंकि बचत समय अभी भी काम नहीं करता है।

ऐसा करने के लिए, मैंने एक सीधी रेखा और एक चक्र के चौराहे को खोजने के लिए प्रसिद्ध फार्मूले का उपयोग किया, जो हर किसी को ज्यामिति में एक स्कूल पाठ्यक्रम से दिल से याद करता है ... किसी की काल्पनिक दुनिया में। मुझे अभी उसकी याद नहीं आई, इसलिए मुझे इसे गूगल करना पड़ा।

हम सांकेतिक शब्दों में बदलना, देखो क्या हुआ।
( डेमो )
यह आदर्श प्रतीत होता है। अब हम जानते हैं कि कौन से सेगमेंट में छाया डाला जा सकता है और राककास्ट कर सकते हैं।

यहां हमारे पास विकल्प भी हैं:

  1. हम बस एक सर्कल में एक सर्कल में जाते हैं, किरणों को फेंकते हैं और चौराहों की तलाश करते हैं। निकटतम चौराहे की दूरी हमारे लिए आवश्यक मूल्य है
  2. आप केवल उन कोनों पर जा सकते हैं जो खंडों में आते हैं। आखिरकार, हम पहले से ही अंक जानते हैं, कोणों की गणना करना मुश्किल नहीं है।
  3. इसके अलावा, यदि हम एक खंड के साथ जाते हैं, तो हमें किरणों को डालने और चौराहों की गणना करने की आवश्यकता नहीं है - हम वांछित कदम के साथ खंड के साथ आगे बढ़ सकते हैं। यहां बताया गया है कि यह कैसे काम करता है:


यहां AB- खंड (दीवार), प्रकाश स्रोत का केंद्र है, - सेगमेंट के लंबवत।

चलो x- सामान्य से कोण, जिसके लिए आपको स्रोत से खंड तक की दूरी ज्ञात करने की आवश्यकता है, X1- खंड पर बिंदु ABजहां किरण गिरती है। त्रिकोण CDX1- आयताकार - एक पैर, और इसकी लंबाई ज्ञात है और इस सेगमेंट के लिए स्थिर है, CX1- वांछित लंबाई। CX1= fracCDcos(x)। यदि आप पहले से कदम जानते हैं (और हम इसे जानते हैं), तो आप उलटे कोसाइन की तालिका की पूर्व-गणना कर सकते हैं और जल्दी से दूरी की तलाश कर सकते हैं।

मैं ऐसी तालिका के लिए कोड का एक उदाहरण दूंगा। कोनों के साथ लगभग सभी काम को अनुक्रमित के साथ काम से बदल दिया जाता है, अर्थात। पूर्णांक 0 से एन, जहां एन = सर्कल में चरणों की संख्या (यानी चरण कोण =)  frac2 piN)

 class HypTable { constructor(steps = 512, stepAngle = 2*Math.PI/steps) { this.perAngleStep = [1]; for (let i = 1; i < steps/4; i++) { //   pi/2 let ang = i*stepAngle; this.perAngleStep[i] = 1/Math.cos(ang); } this.stepAngle = stepAngle; } /** * @param distancesMap -  ,    * @param angle1 -           * @param angle2 -           * @param normalFromLight - ,      */ fillDistancesForArc(distancesMap, angle1, angle2, normalFromLight) { const D = Math.hypot(normalFromLight.x, normalFromLight.y); const normalAngle = Phaser.Math.normalizeAngle(Math.atan2(normalFromLight.y, normalFromLight.x)); const normalAngleIndex = (normalAngle / this.stepAngle)|0; const index1 = (angle1 / this.stepAngle)|0; const index2 = (angle2 / this.stepAngle)|0; for (let angleIndex = index1; angleIndex <= index2; angleIndex++) { let distanceForAngle = D * this.perAngleStep[normalize(angleIndex - normalAngleIndex)]; distancesMap.set(angleIndex, distanceForAngle); } } } 

बेशक, यह विधि उन मामलों के लिए एक त्रुटि पेश करती है जहां प्रारंभिक कोण एसीडी एक चरण का एक बहु नहीं है। लेकिन 512 चरणों के लिए, मैं नेत्रहीन रूप से कोई अंतर नहीं देखता हूं।

तो हम पहले से ही जानते हैं कि कैसे करना है:
  1. प्रकाश स्रोत की सीमा के भीतर सेगमेंट ढूंढें जो एक छाया डाल सकते हैं
  2. चरण टी के लिए, एक सेगमेंट (कोण) तालिका बनाएं, प्रत्येक सेगमेंट से गुजरते हुए और दूरी की गणना करें।


यहाँ पर यह तालिका दिखती है जैसे कि आप इसे किरणों में खींचते हैं।

( डेमो )

और यहां बताया गया है कि यह 10 प्रकाश स्रोतों के लिए कैसा दिखता है, अगर एक बनावट में लिखा गया हो।

यहां, प्रत्येक क्षैतिज पिक्सेल एक कोण से मेल खाती है, और पिक्सेल में दूरी के लिए रंग।
यह इस तरह लिखा है js में इमेजडाटा का उपयोग करके
  fillBitmap(data, index) { let total = index + this.steps*4; let d1, d2; let i = 0; //data[index] = Red //data[index+1] = Green //data[index+2] = Blue //data[index+3] = Alpha for (; index < total; index+=4, i++) { //  512,    R     2. d1 = (this.distances[i]/2)|0; data[index] = d1; d1 = this.distances[i] - d1*2; d2 = (d1*128)|0; //   G -     2. data[index+1] = d2; //  B  A  255,     . data[index+2] = 255; data[index+3] = 255; } } 


अब हम अपने शेडर को बनावट पास करते हैं, जिसमें पहले से ही प्रकाश स्रोतों के निर्देशांक और त्रिज्या हैं। और इसे इस तरह से प्रोसेस करें:

 //      uniform sampler2D iChannel0; #define STRENGTH 0.3 #define MAX_DARK 0.7 #define M_PI 3.141592653589793 #define M_PI2 6.283185307179586 //       float decodeDist(vec4 color) { return color.r*255.*2. + color.g*2.; } float getShadow(int i, float angle, float distance) { //   x   ==  float u = angle/M_PI2; //   y   ==     float v = float(i)/${MAX_LIGHTS}.; float shadowAfterDistance = decodeDist(texture2D(iChannel0, vec2(u, v))); //  1   ,  0  . return step(shadowAfterDistance, distance); } void main() { float lightness = 0.; for (int i = 0; i < ${MAX_LIGHTS}; i++) { if (i >= lightsCount) break; vec4 light = lights[i]; //       vec2 light2point = gl_FragCoord.xy - light.xy; float radius = light.z; float distance = length(light2point); float inLight = step(distance, radius); //      ,       //  . //      , //    ,          //           //     ,    if (inLight == 0.) continue; float angle = mod(-atan(light2point.y, light2point.x), M_PI2); // 1     0   float thisLightness = (1. - getShadow(i, angle, distance)); //,   “”  ,   ,  //    lightness += thisLightness*STRENGTH; } lightness = clamp(0., 1., lightness); gl_FragColor = mix(vec4(0,0,0,MAX_DARK), vec4(0,0,0,0), lightness); } 


परिणाम:
( डेमो )
अब आप थोड़ी सुंदरता ला सकते हैं। दूरी के साथ प्रकाश फीका होने दें, और छाया धुंधली हो जाएगी।

धुंधला के लिए, मैं आसन्न कोनों को देखता हूं, + - चरण, इस तरह से:

 thisLightness = (1. - getShadow(i, angle, distance)) * 0.4 + (1. - getShadow(i, angle-SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle+SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle-SMOOTH_STEP*2., distance)) * 0.1 + (1. - getShadow(i, angle+SMOOTH_STEP*2., distance)) * 0.1; 


यदि आप सब कुछ एक साथ रखते हैं और एफपीएस को मापते हैं, तो यह इस तरह निकलता है:

  • अंतर्निहित वीडियो कार्ड पर - सब कुछ खराब है (<30-40), यहां तक ​​कि सरल उदाहरणों के लिए भी
  • बाकी सब कुछ ठीक है, जब तक कि प्रकाश स्रोत बहुत मजबूत नहीं हैं। यानी, प्रति पिक्सेल प्रकाश स्रोतों की संख्या महत्वपूर्ण है, कुल संख्या नहीं।


यह परिणाम मेरे लिए काफी अनुकूल था। आप अभी भी प्रकाश के रंग के साथ खेल सकते हैं, लेकिन मैंने नहीं किया। थोड़ा मुड़ने और कुछ सामान्य नक्शे जोड़ने के बाद, मैंने NOPE का एक अद्यतन संस्करण अपलोड किया। वह अब इस तरह दिखती थी:


फिर उन्होंने एक लेख तैयार करना शुरू किया। मैंने ऐसे ही जिफ और विचार से देखा।

"तो यह लगभग एक छद्म 3 डी रूप है, जैसे कि वोल्फेंस्टीन में," मैंने कहा (हाँ, मेरे पास एक अच्छी कल्पना है)। और वास्तव में - अगर हम मान लें कि सभी दीवारें समान ऊँचाई हैं, तो दृश्य निर्माण के लिए दूरी के नक्शे हमारे लिए पर्याप्त होंगे। कोशिश क्यों नहीं की?

दृश्य को कुछ इस तरह देखना चाहिए।


तो हमारा काम:

  1. स्क्रीन पर एक बिंदु पर, मामले के लिए दुनिया के निर्देशांक प्राप्त करें जब कोई दीवार नहीं होती है।

    हम इस पर विचार करेंगे:
    • सबसे पहले, हम स्क्रीन पर एक बिंदु के निर्देशांक को सामान्य करते हैं ताकि स्क्रीन के केंद्र में एक बिंदु (0,0) हो, और कोनों (-1, -1) और (1,1) पर, क्रमशः
    • X निर्देशांक कोण को देखने की दिशा से कोण बन जाता है, आपको बस इसे A / 2 से गुणा करना होगा, जहाँ A देखने का कोण है
    • Y निर्देशांक पर्यवेक्षक से बिंदु तक की दूरी को निर्धारित करता है, सामान्य मामले में d ~ 1 / y। स्क्रीन के निचले किनारे पर एक बिंदु के लिए, दूरी = 1, स्क्रीन के केंद्र में एक बिंदु के लिए, दूरी = अनंत।
    • इस प्रकार, यदि आप दीवारों को ध्यान में नहीं रखते हैं, तो दुनिया में प्रत्येक दृश्य बिंदु के लिए स्क्रीन पर 2 बिंदु होंगे - बीच में एक ऊपर ("छत" पर) और दूसरा नीचे ("मंजिल" पर)
  2. अब हम दूरियों की तालिका को देख सकते हैं। यदि हमारे बिंदु से करीब कोई दीवार है, तो आपको एक दीवार खींचने की आवश्यकता है। यदि नहीं, तो इसका मतलब फर्श या छत है

हमें आदेश दिया गया है:
( डेमो )
प्रकाश जोड़ें - उसी तरह, प्रकाश स्रोतों पर पुनरावृति करें और दुनिया के निर्देशांक की जांच करें। और - अंतिम स्पर्श - बनावट जोड़ें। ऐसा करने के लिए, दूरी के साथ एक बनावट में, आपको इस बिंदु पर दीवार की बनावट के लिए ऑफसेट यू लिखना होगा। यहीं पर चैनल बी काम आया।
( डेमो )
आदर्श।

बस मजाक कर रहे हैं।

निःसंदेह, अपूर्ण। लेकिन नरक, मैंने अभी भी लगभग 15 साल पहले रैकोकास्ट के माध्यम से अपने वुल्फेनस्टीन बनाने के तरीके के बारे में पढ़ा, और मैं यह सब करना चाहता था, और यहाँ ऐसा अवसर है!

एक निष्कर्ष के बजाय


लेख की शुरुआत में, मैंने एक और गुप्त विधि का उल्लेख किया। यहाँ यह है:

बस उस इंजन को लें जो पहले से ही जानता है कि कैसे।

वास्तव में, यदि आपको एक खेल बनाने की आवश्यकता है, तो यह सबसे सही और तेज़ तरीका होगा। आपको अपनी बाइक को बाड़ लगाने और लंबे समय तक चलने वाली समस्याओं को हल करने की आवश्यकता क्यों है?

लेकिन क्यों।

10 वीं कक्षा में, मैं दूसरे स्कूल में चला गया और गणित में समस्याओं में भाग गया। मुझे सटीक उदाहरण याद नहीं है, लेकिन यह डिग्री के साथ एक समीकरण था, जिसे सभी मामलों में सरल बनाने की आवश्यकता थी, लेकिन यह अभी सफल नहीं हुआ। हताश, मैंने अपनी बहन से सलाह ली, और उसने कहा: "इसलिए दोनों पक्षों पर एक्स 2 जोड़ें, और सब कुछ विघटित हो जाएगा।" और वह समाधान था: जो नहीं था, उसे जोड़ें।

जब, बहुत बाद में, मैंने अपने दोस्त को अपना घर बनाने में मदद की, मुझे दहलीज पर एक ब्लॉक लगाने की ज़रूरत थी - एक जगह भरने के लिए। और यहां मैं खड़ा हूं और सलाखों के ट्रिम को छांट रहा हूं। एक फिट लगता है, लेकिन काफी नहीं है। अन्य बहुत छोटे हैं। मैं इस बारे में सोच रहा हूं कि यहां शब्द को कैसे इकट्ठा किया जाए, और एक दोस्त कहता है: "इसलिए उन्होंने खांचे को एक गोलाकार जगह पर पिया जहां यह हस्तक्षेप करता है"। और अब बड़ी पट्टी पहले से ही खड़ी है।

ये कहानियाँ इस तरह के प्रभाव से एकजुट हैं, जिसे मैं "इन्वेंट्री प्रभाव" कहूंगा। जब आप मौजूदा भागों से निर्णय लेने की कोशिश करते हैं, तो उन सामग्रियों को देखे बिना जिन्हें इन भागों में संसाधित और परिष्कृत किया जा सकता है। नंबर लकड़ी, पैसे या कोड हैं।

कई बार मैंने प्रोग्रामिंग में सहकर्मियों के साथ समान प्रभाव देखा है। सामग्री में आत्मविश्वास महसूस नहीं होने पर, वे कभी-कभी ऐसा करते हैं जब ऐसा करना आवश्यक होता है, कहते हैं, गैर-मानक नियंत्रण। या जहां वे नहीं थे, वहां यूनिट टेस्ट जोड़ें। या वे एक कक्षा को डिजाइन करते समय हर चीज, हर चीज के लिए प्रदान करने की कोशिश करते हैं और फिर हमें एक संवाद मिलता है जैसे:
- यह अब आवश्यक नहीं है
- यदि यह आवश्यक हो जाए तो क्या होगा?
- फिर हम जोड़ देंगे। विस्तार बिंदुओं को छोड़ दें, बस इतना ही। कोड ग्रेनाइट नहीं है, यह प्लास्टिसिन है।

और हमारे द्वारा काम की जाने वाली सामग्री को देखने और महसूस करने के लिए सीखने के लिए, हमें साइकिल की भी आवश्यकता है।

यह केवल दिमाग या प्रशिक्षण के लिए एक कसरत नहीं है। यह कोड के साथ गुणात्मक रूप से भिन्न स्तर तक पहुंचने का एक तरीका है।

आप सभी को पढ़ने के लिए धन्यवाद।

लिंक, यदि आप कहीं क्लिक करना भूल गए हैं:

Source: https://habr.com/ru/post/hi434370/


All Articles