शेडर के बारे में जानकारी

परिचय

सबसे पहले, Three.js के बारे में जानकारी पढ़ें, क्योंकि यह इस दस्तावेज़ का आधार है.

मुझे शेडर के बारे में बात करनी है. WebGL बहुत शानदार है और जैसा कि मैंने पहले भी कहा है कि Three.js (और अन्य लाइब्रेरी) आपके लिए मुश्किलों को दूर करने का शानदार काम करती हैं. लेकिन कई बार ऐसा होता है कि आपको कोई खास इफ़ेक्ट चाहिए होता है या आपको यह जानना होता है कि आपकी स्क्रीन पर वह शानदार चीज़ कैसे दिख रही है. ऐसे में, शेडर का इस्तेमाल करना ज़रूरी हो जाता है. पिछले ट्यूटोरियल में आपने बेसिक चीज़ें सीखीं. अब आपको कुछ मुश्किल चीज़ें सीखनी चाहिए.

मैं इस आधार पर काम करूंगा कि आप Three.js का इस्तेमाल कर रहे हैं, क्योंकि यह शेडर को चालू करने के मामले में हमारे लिए बहुत काम करता है. इस दस्तावेज़ में, आपको पहले शेडर के बारे में जानकारी मिलेगी. इसके बाद, ऐडवांस चरणों के बारे में बताया जाएगा.

1. हमारे दो शेडर

WebGL, फ़िक्स्ड पाइपलाइन का इस्तेमाल करने की सुविधा नहीं देता. इसका मतलब है कि यह आपको अपने कॉन्टेंट को रेंडर करने का कोई आसान तरीका नहीं देता. यह प्रोग्रामेबल पाइपलाइन की सुविधा देता है. यह ज़्यादा बेहतर है, लेकिन इसे समझना और इस्तेमाल करना ज़्यादा मुश्किल है. प्रोग्रामेबल पाइपलाइन का मतलब है कि प्रोग्रामर के तौर पर, स्क्रीन पर वर्टिसेस वगैरह को रेंडर करने की ज़िम्मेदारी आपकी होती है. शेडर इस पाइपलाइन का हिस्सा हैं. ये दो तरह के होते हैं:

  1. वर्टेक्स शेडर
  2. फ़्रैगमेंट शेडर

मुझे पूरा भरोसा है कि आप इस बात से सहमत होंगे कि इन दोनों का अपने-आप में कोई मतलब नहीं है. आपको इनके बारे में यह पता होना चाहिए कि ये दोनों आपके ग्राफ़िक्स कार्ड के जीपीयू पर पूरी तरह से काम करते हैं. इसका मतलब है कि हम ज़्यादा से ज़्यादा काम जीपीयू को सौंपना चाहते हैं, ताकि सीपीयू दूसरे काम कर सके. एक आधुनिक GPU को शेडर्स के लिए ज़रूरी फ़ंक्शन के लिए काफ़ी ऑप्टिमाइज़ किया गया है. इसलिए, इसका इस्तेमाल करना बहुत अच्छा है.

2. वर्टेक्स शेडर

कोई स्टैंडर्ड प्रिमिटिव शेप लें, जैसे कि स्फ़ियर. यह वर्टिसेस से बना है, है न? वर्टेक्स शेडर को इनमें से हर वर्टेक्स को बारी-बारी से दिया जाता है और वह उनके साथ छेड़छाड़ कर सकता है. यह वर्टेक्स शेडर पर निर्भर करता है कि वह हर एक के साथ क्या करता है. हालांकि, उसकी एक ज़िम्मेदारी होती है: उसे किसी समय gl_Position को सेट करना होता है. यह एक 4D फ़्लोट वेक्टर होता है, जो स्क्रीन पर वर्टेक्स की आखिरी पोज़िशन होती है. यह अपने आप में काफ़ी दिलचस्प प्रोसेस है, क्योंकि हम असल में 2D स्क्रीन पर 3D पोज़िशन (x, y, z के साथ एक वर्टेक्स) को दिखाने या प्रोजेक्ट करने के बारे में बात कर रहे हैं. हमारे लिए अच्छी बात यह है कि अगर हम Three.js जैसी किसी चीज़ का इस्तेमाल कर रहे हैं, तो हमारे पास gl_Position को सेट करने का एक आसान तरीका है. इससे चीज़ें ज़्यादा मुश्किल नहीं होतीं.

3. फ़्रैगमेंट शेडर

तो हमारे पास वर्टिस के साथ हमारा ऑब्जेक्ट है और हमने उन्हें 2D स्क्रीन पर प्रोजेक्ट किया है, लेकिन हम जिन रंगों का इस्तेमाल करते हैं उनके बारे में क्या? टेक्सचर और लाइटिंग के बारे में क्या? फ़्रैगमेंट शेडर का काम यही है. वर्टेक्स शेडर की तरह ही, फ़्रैगमेंट शेडर का भी सिर्फ़ एक ज़रूरी काम होता है: इसे gl_FragColor वैरिएबल को सेट करना या खारिज करना होता है. यह एक और 4D फ़्लोट वेक्टर होता है, जो हमारे फ़्रैगमेंट का फ़ाइनल कलर होता है.

लेकिन फ़्रैगमेंट क्या होता है? तीन शीर्षों के बारे में सोचें जिनसे एक त्रिभुज बनता है. उस त्रिभुज के हर पिक्सल को बनाना होगा. फ़्रैगमेंट वह डेटा होता है जो उन तीन वर्टिसेस से मिलता है. इसका इस्तेमाल, उस ट्रायंगल में हर पिक्सल को ड्रॉ करने के लिए किया जाता है. इस वजह से, फ़्रैगमेंट को उनके कॉम्पोनेंट वर्टिस से इंटरपोलेट की गई वैल्यू मिलती हैं. अगर एक वर्टेक्स लाल रंग का है और उसके बगल वाला नीला है, तो हमें रंग की वैल्यू लाल से बैंगनी और फिर नीले रंग में बदलती हुई दिखेगी.

4. शेडर वैरिएबल

आपके पास तीन तरह के वैरिएबल डिक्लेयर करने का विकल्प है: यूनिफ़ॉर्म, एट्रिब्यूट, और वैरिएबल. इनसे भ्रम हो सकता है, क्योंकि ये किसी भी अन्य सामान्य भाषा से मेल नहीं खाते. इनके बारे में इस तरह से सोचा जा सकता है:

  1. यूनिफ़ॉर्म, वर्टेक्स शेडर और फ़्रैगमेंट शेडर, दोनों को भेजे जाते हैं. इनमें ऐसी वैल्यू होती हैं जो रेंडर किए जा रहे पूरे फ़्रेम में एक जैसी रहती हैं. इसका एक अच्छा उदाहरण, लाइट की पोज़िशन हो सकती है.

  2. एट्रिब्यूट ऐसी वैल्यू होती हैं जो अलग-अलग वर्टिस पर लागू होती हैं. एट्रिब्यूट सिर्फ़ वर्टेक्स शेडर के लिए उपलब्ध होते हैं. जैसे, हर वर्टेक्स का रंग अलग हो. एट्रिब्यूट का वर्टिस के साथ वन-टू-वन रिलेशनशिप होता है.

  3. Varying, वर्टेक्स शेडर में डिक्लेयर किए गए ऐसे वैरिएबल होते हैं जिन्हें हमें फ़्रैगमेंट शेडर के साथ शेयर करना होता है. इसके लिए, हम यह पक्का करते हैं कि वर्टेक्स शेडर और फ़्रैगमेंट शेडर, दोनों में एक ही टाइप और नाम का वैरिएबल तय किया गया हो. इसका एक क्लासिक इस्तेमाल वर्टेक्स का नॉर्मल होगा, क्योंकि इसका इस्तेमाल लाइटिंग कैलकुलेशन में किया जा सकता है.

बाद में, हम तीनों टाइप का इस्तेमाल करेंगे, ताकि आपको पता चल सके कि उन्हें असल में कैसे लागू किया जाता है.

नहीं, सबसे आसान शेडर में से एक पर नज़र डालें.

5. Bonjourno 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 color 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 color
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);
}

अब हमें एक टूटा-फूटा गोला दिख रहा है. हालांकि, अच्छी बात यह है कि सारा डिसप्लेसमेंट जीपीयू पर हो रहा है.

11. इसे ऐनिमेट करें

हमें इसे ऐनिमेट करना चाहिए. हम ऐसा कैसे करते हैं? इसके लिए, हमें दो चीज़ों की ज़रूरत होगी:

  1. एक यूनिफ़ॉर्म, जिससे यह ऐनिमेट किया जा सके कि हर फ़्रेम में कितना डिसप्लेसमेंट लागू किया जाना चाहिए. इसके लिए, हम sine या cosine का इस्तेमाल कर सकते हैं, क्योंकि ये -1 से 1 तक चलते हैं
  2. 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()
});

फ़िलहाल, हमारे शेडर तैयार हैं. लेकिन अभी ऐसा लगता है कि हम एक कदम पीछे चले गए हैं. ऐसा इसलिए है, क्योंकि हमारे ऐम्प्लिट्यूड की वैल्यू 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);

नतीजा

यह बहुत आसान है! अब आपको यह अजीब (और थोड़ा अजीब) तरीके से धड़कता हुआ दिखेगा.

शेडर के बारे में बताने के लिए और भी बहुत कुछ है. हमें उम्मीद है कि आपको यह जानकारी मददगार लगी होगी. अब आपको शेडर के बारे में जानकारी मिल गई होगी. साथ ही, आपको खुद के शानदार शेडर बनाने का आत्मविश्वास भी मिला होगा.