परिचय
मैंने पहले आपको Three.js के बारे में जानकारी दी है. अगर आपने यह नहीं पढ़ा है कि आपको इस लेख के दौरान, इस बुनियाद को जोड़ना है, तो आपको ऐसा करना चाहिए.
मुझे शेडर पर चर्चा करनी है. WebGL बहुत बढ़िया है. मैंने पहले भी बताया है कि Three.js (और अन्य लाइब्रेरी) आपके लिए मुश्किलों को हल करने का बेहतरीन काम करती हैं. हालांकि, कभी-कभी आपको कोई खास इफ़ेक्ट चाहिए होगा या आपको यह जानना होगा कि स्क्रीन पर वह शानदार चीज़ कैसे दिखी. ऐसे में, शेडर का इस्तेमाल करना ज़रूरी हो जाता है. साथ ही, अगर आप मेरी तरह हैं, तो हो सकता है कि आप पिछले ट्यूटोरियल की बुनियादी चीज़ों से कुछ ज़्यादा पेचीदा काम करना चाहें. हम इस आधार पर काम करेंगे कि आपने Three.js का इस्तेमाल किया है, क्योंकि यह शेडर को चालू करने के लिए, हमारे लिए बहुत काम करता है. मैं आपको पहले ही बता दूं कि शुरुआत में, मैं शेडर के बारे में बताऊंगा. साथ ही, इस ट्यूटोरियल के आखिरी हिस्से में, हम थोड़ी ज़्यादा बेहतर जानकारी देंगे. इसकी वजह यह है कि शेडर पहली नज़र में असामान्य होते हैं और थोड़ी बहुत जानकारी देते हैं.
1. हमारे टू शेडर
WebGL में, फ़िक्स्ड पाइपलाइन का इस्तेमाल नहीं किया जा सकता. इसका मतलब है कि यह आपको अपने कॉन्टेंट को रेंडर करने का कोई तरीका नहीं देता. हालांकि, इसमें प्रोग्रामेबल पाइपलाइन की सुविधा उपलब्ध है. यह सुविधा ज़्यादा बेहतर है, लेकिन इसे समझना और इस्तेमाल करना ज़्यादा मुश्किल है. कम शब्दों में, प्रोग्रामेबल पाइपलाइन का मतलब है कि प्रोग्रामर के तौर पर, आपको स्क्रीन पर रेंडर किए गए वर्टिसेस वगैरह की ज़िम्मेदारी लेनी होगी. शेडर इस पाइपलाइन का हिस्सा होते हैं और ये दो तरह के होते हैं:
- वर्टेक्स शेडर
- फ़्रैगमेंट शेडर
मुझे यकीन है कि आप इस बात से सहमत होंगे कि इन दोनों में से किसी का भी कोई मतलब नहीं है. आपको इनके बारे में यह जानकारी होनी चाहिए कि ये दोनों पूरी तरह से आपके ग्राफ़िक कार्ड के जीपीयू पर काम करते हैं. इसका मतलब है कि हम ज़्यादा से ज़्यादा काम, एआई को करने के लिए देना चाहते हैं, ताकि हमारा सीपीयू दूसरे काम कर सके. आधुनिक जीपीयू, उन फ़ंक्शन के लिए काफ़ी ऑप्टिमाइज़ किया गया है जिनकी शेडर को ज़रूरत होती है. इसलिए, इसका इस्तेमाल करना बेहतर होता है.
2. वर्टेक्स शेडर
कोई स्टैंडर्ड प्रिमिटिव आकार चुनें, जैसे कि गोला. यह वर्टिसेस से बना होता है, है न? वर्टिक्स शेडर को इनमें से हर एक वर्टिक्स को बारी-बारी से दिया जाता है और इनमें बदलाव किया जा सकता है. यह वर्टिक्स शेडर पर निर्भर करता है कि वह हर वर्टिक्स के साथ क्या करता है. हालांकि, इसकी एक ज़िम्मेदारी है: उसे किसी समय gl_Position नाम का एक 4D फ़्लोट वेक्टर सेट करना होगा. यह स्क्रीन पर वर्टिक्स की फ़ाइनल पोज़िशन होती है. यह एक दिलचस्प प्रोसेस है, क्योंकि हम असल में 2D स्क्रीन पर 3D पोज़िशन (x,y,z वाला वर्टिक्स) पाने या प्रोजेक्ट करने के बारे में बात कर रहे हैं. हमारे लिए अच्छी बात यह है कि अगर हम Three.js जैसे किसी टूल का इस्तेमाल कर रहे हैं, तो हमारे पास gl_Position को सेट करने का एक आसान तरीका होगा.
3. फ़्रैगमेंट शेडर
हमारे पास अपने ऑब्जेक्ट के साथ कोने हैं और हमने उन्हें 2D स्क्रीन पर प्रोजेक्ट किया है, लेकिन हम जिन रंगों का इस्तेमाल करते हैं उनका क्या होगा? टेक्सचर और लाइटिंग के बारे में क्या? फ़्रैगमेंट शेडर का यही काम है. वर्टिक्स शेडर की तरह ही, फ़्रैगमेंट शेडर का भी सिर्फ़ एक काम होता है: उसे gl_FragColor वैरिएबल को सेट या खारिज करना होता है. यह एक और 4D फ़्लोट वेक्टर है, जो हमारे फ़्रैगमेंट का फ़ाइनल रंग होता है. लेकिन एक फ़्रैगमेंट क्या होता है? तीन ऐसे वर्टिसेस के बारे में सोचें जो एक त्रिभुज बनाते हैं. उस ट्रायंगल में मौजूद हर पिक्सल को बाहर निकालना होगा. फ़्रैगमेंट, उन तीन वर्टिसेस से मिलने वाला डेटा होता है. इसका इस्तेमाल, त्रिभुज के हर पिक्सल को ड्रॉ करने के लिए किया जाता है. इस वजह से, फ़्रैगमेंट को उनके कॉम्पोनेंट वर्टिसेस से इंटरपोलेशन की गई वैल्यू मिलती हैं. अगर किसी शीर्ष का रंग लाल है और उसका पड़ोसी नीला है, तो हम देखेंगे कि रंग के मान लाल से बैंगनी रंग से नीले रंग में बदलते हैं.
4. शेडर वैरिएबल
वैरिएबल के बारे में बात करते समय, तीन एलान किए जा सकते हैं: यूनिफ़ॉर्म, एट्रिब्यूट, और अलग-अलग वैल्यू. जब मैंने पहली बार इन तीनों के बारे में सुना, तो मुझे काफ़ी उलझन हुई, क्योंकि ये उन सभी चीज़ों से मेल नहीं खाते जिन पर मैंने कभी काम किया है. हालांकि, इनका इस्तेमाल इस तरह किया जा सकता है:
यूनिफ़ॉर्म, वर्टिक्स शेडर और फ़्रैगमेंट शेडर, दोनों को भेजे जाते हैं. इनमें ऐसी वैल्यू होती हैं जो रेंडर किए जा रहे पूरे फ़्रेम में एक जैसी रहती हैं. इसका एक अच्छा उदाहरण, लाइट की पोज़िशन हो सकती है.
एट्रिब्यूट ऐसी वैल्यू होती हैं जिन्हें अलग-अलग वर्टिसेस पर लागू किया जाता है. एट्रिब्यूट सिर्फ़ वर्टेक्स शेडर के लिए उपलब्ध हैं. जैसे, हर वर्टिक्स का अलग रंग होना. एट्रिब्यूट का वर्टिसेस से वन-टू-वन रिलेशनशिप होता है.
बदले जा सकने वाले वैरिएबल, वे वैरिएबल होते हैं जिन्हें वर्टिक्स शेडर में एलान किया जाता है और जिन्हें हमें फ़्रैगमेंट शेडर के साथ शेयर करना होता है. ऐसा करने के लिए, हम यह पक्का करते हैं कि वर्टिक्स शेडर और फ़्रेगमेंट शेडर, दोनों में एक ही टाइप और नाम का वैरिएबल इस्तेमाल किया गया हो. इसका क्लासिक इस्तेमाल, किसी वर्टिक्स के सामान्य के तौर पर किया जा सकता है, क्योंकि इसका इस्तेमाल लाइटिंग कैलकुलेशन में किया जा सकता है.
बाद में, हम तीनों टाइप का इस्तेमाल करेंगे, ताकि आपको पता चल सके कि इन्हें असल में कैसे लागू किया जाता है.
अब हमने वर्टेक्स शेडर और फ़्रैगमेंट शेडर के बारे में बात की है. साथ ही, यह भी बताया कि वे किस तरह के वैरिएबल का इस्तेमाल करते हैं. अब हम सबसे आसान शेडर बना सकते हैं.
5. Bonjourno World
यहां, वर्टिक्स शेडर का Hello World है:
/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
यही तरीका फ़्रैगमेंट शेडर के लिए भी है:
/**
* Set the colour to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
हालांकि, यह बहुत मुश्किल नहीं है, है न?
वर्टिक्स शेडर में, हमें Three.js से कुछ यूनिफ़ॉर्म भेजे जाते हैं. ये दोनों यूनिफ़ॉर्म, 4D मैट्रिक्स हैं. इन्हें मॉडल-व्यू मैट्रिक्स और प्रोजेक्शन मैट्रिक्स कहा जाता है. आपको इनके काम करने के तरीके के बारे में पूरी जानकारी ज़रूरी नहीं है. हालांकि, अगर हो सके, तो यह समझना हमेशा बेहतर होता है कि चीज़ें कैसे काम करती हैं. कम शब्दों में कहें, तो वर्टेक्स की 3D स्थिति को स्क्रीन पर आखिरी 2D स्थिति में दिखाया जाता है.
मैंने उन्हें ऊपर दिए गए स्निपेट में शामिल नहीं किया है, क्योंकि Three.js उन्हें आपके शेडर कोड में सबसे ऊपर जोड़ता है. इसलिए, आपको ऐसा करने की ज़रूरत नहीं है. असल में, इसमें इससे ज़्यादा जानकारी जोड़ी जाती है. जैसे, लाइट डेटा, वर्टिक्स के रंग, और वर्टिक्स के सामान्य आकार. अगर Three.js के बिना ऐसा किया जा रहा था, तो आपको उन सभी यूनिफ़ॉर्म और एट्रिब्यूट को खुद बनाना और सेट करना होगा. सच्ची कहानी.
6. MeshShaderMaterial का इस्तेमाल करना
ठीक है, हमने शेडर सेट अप कर लिया है, लेकिन Three.js के साथ इसका इस्तेमाल कैसे करें? यह ऐसा होता है:
/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader: $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});
इसके बाद, Three.js उस मेश से जुड़े आपके शेडर को कॉम्पाइल और चलाएगा जिसे आपने वह मटीरियल दिया है. असल में, इससे ज़्यादा आसान कुछ भी नहीं है. वैसे तो यह काम करता होगा, लेकिन हम आपके ब्राउज़र में 3D चलाने के बारे में बात कर रहे हैं, इसलिए मुझे लगता है कि आपको कुछ जटिल चीज़ों की उम्मीद है.
हम अपने MeshShaderMaterial में दो और प्रॉपर्टी जोड़ सकते हैं: यूनिफ़ॉर्म और एट्रिब्यूट. दोनों में वैक्टर, पूर्णांक या फ़्लोट इस्तेमाल किए जा सकते हैं. हालांकि, जैसा कि मैंने पहले बताया था कि यूनिफ़ॉर्म पूरे फ़्रेम के लिए एक जैसे होते हैं, यानी सभी वर्टिसेस के लिए. इसलिए, वे एक ही वैल्यू के होते हैं. हालांकि, एट्रिब्यूट हर वर्टेक्स वैरिएबल होते हैं, इसलिए उन्हें एक कलेक्शन होना चाहिए. एट्रिब्यूट ऐरे में मौजूद वैल्यू की संख्या और मेश में मौजूद वर्टिसेस की संख्या के बीच एक-एक का संबंध होना चाहिए.
7. अगले चरण
अब हम ऐनिमेशन लूप, वर्टिक्स एट्रिब्यूट, और यूनिफ़ॉर्म जोड़ने में थोड़ा समय बिताएंगे. हम अलग-अलग वैरिएबल भी जोड़ेंगे, ताकि वर्टेक्स शेडर फ़्रैगमेंट शेडर में कुछ डेटा भेज सके. अंतिम परिणाम यह है कि हमारा जो गोला गुलाबी था, वह ऊपर और किनारे से प्रकाशित होता हुआ दिखेगा और चमकने लगेगा. यह थोड़ा मुश्किल है, लेकिन उम्मीद है कि इससे आपको तीन तरह के वैरिएबल के बारे में अच्छी जानकारी मिलेगी. साथ ही, यह भी पता चलेगा कि ये एक-दूसरे और अंडरलाइंग ज्यामिति से कैसे जुड़े हैं.
8. फ़र्ज़ी लाइट
रंग को अपडेट करें, ताकि यह एक ही रंग का ऑब्जेक्ट न दिखे. हम यह देख सकते हैं कि Three.js, लाइटिंग को कैसे मैनेज करता है. हालांकि, मुझे यकीन है कि आप इस बात से सहमत होंगे कि यह अभी हमारे लिए ज़रूरत से ज़्यादा जटिल है. इसलिए, हम इसे फ़ेक करेंगे. आपको Three.js के शानदार शेडर और क्रिस मिलक और Google के हाल ही के बेहतरीन WebGL प्रोजेक्ट, Rome के शेडर को देखना चाहिए. हमारे शेडर पर वापस जाएं. हम अपने वर्टिक्स शेडर को अपडेट करेंगे, ताकि फ़्रैगमेंट शेडर को हर वर्टिक्स के लिए सामान्य वैल्यू दी जा सके. हम ऐसा इन तरीकों से करते हैं:
// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;
void main() {
// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
और फ़्रैगमेंट शेडर में, हम वही वैरिएबल नाम सेट अप करेंगे और फिर वर्टेक्स के डॉट प्रॉडक्ट का इस्तेमाल ऐसे वेक्टर के साथ करेंगे जो स्फ़ीयर के ऊपर और दाईं ओर चमकने वाली लाइट को दिखाता है. इसका कुल नतीजा हमें 3D पैकेज में दी जाने वाली लाइट की तरह ही दिखता है.
// same name and type as VS
varying vec3 vNormal;
void main() {
// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
// ensure it's normalized
light = normalize(light);
// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));
// feed into our frag colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
इसलिए, डॉट प्रॉडक्ट के काम करने की वजह यह है कि दो वेक्टर के साथ यह एक संख्या के साथ आता है, जिससे आपको पता चलता है कि दोनों वेक्टर कितने 'समान' हैं. नॉर्मलाइज़ किए गए वैक्टर के एक ही दिशा में होने पर, आपको वैल्यू 1 मिलती है. अगर वह व्यक्ति उलटी दिशा में इशारा करता है, तो आपको -1 मिलता है. हम क्या करते हैं कि यह संख्या लेकर उसे अपनी रोशनी में इस्तेमाल करें. इसलिए, सबसे ऊपर दाईं ओर मौजूद किसी वर्टिक्स की वैल्यू 1 के आस-पास या उसके बराबर होगी. इसका मतलब है कि वह पूरी तरह से रोशन होगा. वहीं, किनारे पर मौजूद किसी वर्टिक्स की वैल्यू 0 के आस-पास होगी और पीछे की ओर -1 होगी. हम किसी भी नेगेटिव वैल्यू के लिए, वैल्यू को 0 पर सेट कर देते हैं. हालांकि, संख्याओं को प्लग इन करने पर, आपको वही बुनियादी लाइटिंग दिखती है जो हमें दिख रही है.
आगे क्या करना है? कुछ वर्टिक्स की पोज़िशन में बदलाव करने की कोशिश करना अच्छा होगा.
9. विशेषताएं
अब हमें एट्रिब्यूट की मदद से, हर वर्टेक्स में कोई रैंडम नंबर जोड़ना है. हम इस संख्या का इस्तेमाल वर्टेक्स को सामान्य रूप से बाहर धकेलने के लिए करेंगे. इससे एक अजीब स्पाइक बॉल मिलती है, जो हर बार पेज रीफ़्रेश करने पर बदलती रहती है. फ़िलहाल, इसमें ऐनिमेशन नहीं दिखेगा. यह अगले चरण में दिखेगा. हालांकि, पेज को कुछ बार रीफ़्रेश करने पर, आपको पता चल जाएगा कि यह रैंडमाइज़ किया गया है.
सबसे पहले, वर्टिक्स शेडर में एट्रिब्यूट जोड़ें:
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
normal *
vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
यह कैसा दिखता है?
असल में, बहुत ज़्यादा फ़र्क़ नहीं है! इसकी वजह यह है कि एट्रिब्यूट को MeshShaderMaterial में सेट अप नहीं किया गया है. इसलिए, शेडर इसके बजाय शून्य वैल्यू का इस्तेमाल करता है. फ़िलहाल, यह प्लेसहोल्डर की तरह है. हम अब JavaScript में, MeshShaderMaterial में एट्रिब्यूट जोड़ेंगे. इसके बाद, Three.js इन दोनों को अपने-आप जोड़ देगा.
यह भी ध्यान देने वाली बात है कि मुझे अपडेट की गई पोज़िशन को नए vec3 वैरिएबल को असाइन करना पड़ा, क्योंकि सभी एट्रिब्यूट की तरह ही ओरिजनल एट्रिब्यूट भी रीड ओनली होता है.
10. MeshShaderMaterial अपडेट करना
आइए, सीधे अपने MeshShaderMaterial अच्छे एट्रिब्यूट के साथ अपडेट करने की बात करते हैं जो हमारे विस्थापन को बेहतर बनाने के लिए ज़रूरी है. ध्यान रखें: एट्रिब्यूट में हर वर्टेक्स की वैल्यू होती हैं, इसलिए हमें अपने स्फ़ीयर में हर वर्टेक्स के लिए एक वैल्यू की ज़रूरत होती है. इस तरह:
var attributes = {
displacement: {
type: 'f', // a float
value: [] // an empty array
}
};
// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}
अब हमें एक ऐसा गोला दिख रहा है जिसका आकार बिगड़ गया है. हालांकि, सबसे अच्छी बात यह है कि यह बदलाव, GPU पर हो रहा है.
11. Animating That Sucker
हमें इस इमेज को ऐनिमेट करना चाहिए. हम ऐसा कैसे करते हैं? इसके लिए, हमें दो चीज़ों की ज़रूरत है:
- हर फ़्रेम में कितना डिसप्लेसमेंट लागू किया जाना चाहिए, यह तय करने के लिए यूनिफ़ॉर्म. इसके लिए, हम sine या cosine का इस्तेमाल कर सकते हैं, क्योंकि ये -1 से 1 के बीच होते हैं
- JS में ऐनिमेशन लूप
हम यूनिफ़ॉर्म को, 'MeshShaderMaterial' और 'Vertex Shader', दोनों में जोड़ेंगे. सबसे पहले, वर्टिक्स शेडर:
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
इसके बाद, हम MeshShaderMaterial को अपडेट करते हैं:
// add a uniform for the amplitude
var uniforms = {
amplitude: {
type: 'f', // a float
value: 0
}
};
// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
अभी हमारे शेडर का काम हो गया है. हालांकि, फ़िलहाल ऐसा लग रहा है कि हमने एक कदम पीछे ले लिया है. ऐसा इसलिए है, क्योंकि हमारे ऐम्प्लitude की वैल्यू 0 है और हमने उसे डिसप्लेसमेंट से गुणा किया है, इसलिए हमें कोई बदलाव नहीं दिख रहा है. हमने ऐनिमेशन लूप भी सेट अप नहीं किया है, ताकि हमें कभी भी 0 को किसी और चीज़ में बदलते हुए न दिखे.
अब हमें अपने JavaScript में, रेंडर कॉल को फ़ंक्शन में रैप करना होगा और फिर उसे कॉल करने के लिए, requestAnimationFrame का इस्तेमाल करना होगा. इसके लिए, हमें यूनिफ़ॉर्म की वैल्यू भी अपडेट करनी होगी.
var frame = 0;
function update() {
// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;
renderer.render(scene, camera);
// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);
12. नतीजा
यह बहुत आसान है! अब आपको यह दिखेगा कि इमोजी, एक अजीब तरीके से (और थोड़ा ट्रिप्पी) पल्सेटिंग ऐनिमेशन में दिख रहा है.
एक विषय के तौर पर हम शेडर पर और भी बहुत सी बातें कवर कर सकते हैं लेकिन उम्मीद है कि आपको यह जानकारी काम की लगी होगी. अब आपको शेडर को समझने में आसानी होगी. साथ ही, आपके पास अपने हिसाब से शानदार शेडर बनाने का आत्मविश्वास भी होगा!