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

परिचय

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

नहीं, यहां एक ऐसा सबसे आसान शेडर दिया गया है जिसे बनाया जा सकता है.

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 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 के आस-पास या 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. इसे ऐनिमेट करें

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

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

नतीजा

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

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