परिचय
हमने पहले ही, 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. नतीजा
यह बहुत आसान है! अब आपको यह दिखेगा कि यह इमोजी, अजीब और थोड़ा ट्रिप्पी तरीके से पल्सेटिंग ऐनिमेशन में दिख रहा है.
शेडर के बारे में हम और भी बहुत कुछ बता सकते हैं, लेकिन हमें उम्मीद है कि आपको यह जानकारी मददगार लगी होगी. अब आपको शेडर को समझने में आसानी होगी. साथ ही, आपके पास अपने हिसाब से शानदार शेडर बनाने का आत्मविश्वास भी होगा!