- होमपेज की साधारण CSS
border-radius animation को शुरुआती बिंदु बनाकर, कुछ पंक्तियों के fragment shader से सीधे smooth blob animation बनाया गया
- shader GPU पर pixel coordinates को color में बदलने वाले छोटे program होते हैं, और parallel execution की वजह से तेज़ होते हैं, लेकिन state sharing और high-level abstraction पर सीमाएँ होती हैं
- GLSL का बुनियादी उदाहरण
vUv coordinates को gl_FragColor के RGBA मानों से map करता है, और varying, uniform, vector types, swizzling जैसी syntax को समझने की दिशा में ले जाता है
- circle और blob को
distance(), step(), smoothstep() और Signed Distance Function(SDF) से बनाया जाता है, और कई SDF को min() या smooth minimum से जोड़ा जाता है
u_time से balls की movement बनाई जाती है, और u_mouse को center array में जोड़ने पर यह ऐसा interactive shader बन जाता है जिसमें user mouse से कुछ हिस्सों को नियंत्रित कर सकता है
Shader क्या करते हैं और उनकी सीमाएँ
- shader GPU पर चलने वाले छोटे program हैं, जो कम-से-कम pixel coordinates को input लेकर color output करते हैं
- video games में इनका उपयोग lighting, special effects, cartoon-style rendering जैसे visual effects के लिए होता है, और ये modern game graphics की अहम नींव हैं
- speed का मुख्य कारण यह parallelization है कि वे कई pixels पर एक साथ चलते हैं
- यह परिचय browser-friendly OpenGL Shading Language यानी GLSL पर केंद्रित है
- performance की कीमत पर shader को छोटा और low-level लिखना पड़ता है
- high-level abstraction या library import पर निर्भर रहना मुश्किल होता है
- parallel execution की प्रकृति के कारण pixels के बीच data store या share नहीं किया जा सकता, इसलिए यह stateless और memoryless तरीके से काम करता है
Coordinates को color में बदलने का पहला GLSL उदाहरण
- shader normalized coordinates को RGBA color में बदलते हैं
- coordinates आम तौर पर 0 से 1 के बीच normalized होते हैं
(0, 0) नीचे-बाएँ और (1, 1) ऊपर-दाएँ होता है
- इन coordinates को प्रचलन में
st या uv कहा जाता है
- सबसे सरल उदाहरण एक gradient है, जिसमें x coordinate बढ़ने पर red और y coordinate बढ़ने पर green बढ़ता है
varying vec2 vUv;
void main() {
vec2 st = vUv;
gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
}
- blue gradient को
gl_FragColor = vec4(0.0, 0.0, st.x, 1.0); की तरह blue channel में x coordinate डालकर बनाया जा सकता है
- syntax में अहम तत्व ये हैं
varying: ऐसा input जिसका मान हर pixel पर बदलता है
uniform: ऐसा input जो सभी pixels पर समान रहता है
vec2, vec3, vec4, mat2, mat3: C की तरह type स्पष्ट करने वाले vector और matrix types
- swizzling:
vec4(1, 2, 3, 4).xy की तरह vector का कुछ हिस्सा निकालने की notation
gl_FragColor: main() के अंत में हर pixel का color तय करने वाला output
distance(), step(), smoothstep() से circle बनाना
- circle जैसी sharp shape बनाने के लिए भी
drawCircle() जैसी function नहीं, बल्कि mathematical distance का उपयोग किया जाता है
- current pixel और circle center के बीच की दूरी
distance(vec2 p1, vec2 p2) से निकाली जा सकती है
- अगर distance को सीधे color से map करें तो circular gradient मिलता है, और ठोस circle
step(float threshold, float value) से बनता है
- distance threshold से बड़ा हो तो 1
- नहीं तो 0
step() में transition अचानक होती है, इसलिए circle के किनारों पर aliasing आ सकती है
- अधिक smooth edge के लिए
smoothstep(float t_start, float t_end, float x) का उपयोग किया जा सकता है
Signed Distance Function से shape व्यक्त करना
- Signed Distance Function(SDF) किसी shape से space के किसी point की दूरी को signed distance के रूप में व्यक्त करता है
- shape के अंदर negative
- बाहर positive
- boundary पर 0
- circle का SDF center से दूरी में से radius घटाकर बनाया जा सकता है
float circleSDF(vec2 p, float r) {
return length(p) - r;
}
- pixel position
uv के आधार पर point को shift करने पर UV space में किसी भी position पर मौजूद circle तक की दूरी निकाली जा सकती है
d < 0.0 होने पर pixel circle के अंदर है, इसलिए उसे दूसरे color से भरा जा सकता है
- और भी कई 2D SDF shapes Inigo Quilez की comprehensive list में देखी जा सकती हैं
कई SDF को जोड़कर blob बनाना
- SDF से boolean operations के जरिए नई shape बनाना आसान होता है
- दो SDF का union उनकी distances का
min() लेकर बनाया जा सकता है
- अगर दो shapes में से किसी एक के अंदर हों तो minimum distance negative होगी
- अगर दोनों के बाहर हों तो minimum distance positive होगी
1. - smoothstep() का उपयोग इसलिए किया जाता है क्योंकि step() और smoothstep() threshold से बड़ी distance, यानी shape के बाहर, पर 1 output करते हैं
- साधारण
min() दो circles के मिलने की जगह पर sharp discontinuity बनाता है
- blob की तरह smoothly blend करने के लिए smooth minimum का उपयोग किया जाता है
- अतिरिक्त argument
k smoothness की तीव्रता नियंत्रित करता है
- उदाहरण में polynomial smooth min function का उपयोग किया गया है
float smin(float a, float b, float k)
{
float h = max( k-abs(a-b), 0.0 )/k;
return min( a, b ) - h*h*k*(1.0/4.0);
}
Time uniform से metaball animation बनाना
- shader में slider जैसी arbitrary values को uniform के रूप में भेजा जा सकता है
- animation इस तरह बनाई जाती है कि JavaScript में बना
u_time shader input में दिया जाता है, और उससे circle center coordinates निकाले जाते हैं
- shader आम तौर पर प्रति सेकंड 60 बार नए
u_time value के साथ refresh होता है, जिससे smooth movement मिलती है
- circle centers को
sin, cos जैसे periodic functions से हिलाया जा सकता है
- कई circles को metaball की तरह जोड़ते समय center coordinates को array में रखकर loop से SDF accumulate किया जाता है
vec2 centers[4] = vec2[4](c1,c2,c3,c4);
float d = 99.;
for (int i = 0; i < 4; i++) {
vec2 c = centers[i];
float sdf = circleSDF(uv, c, .1*u_slider);
d = smin(d, sdf, K);
}
- नतीजे में बना blob कार्यात्मक रूप से सही है, लेकिन एक ही रंग का है, इसलिए आगे उसमें color और interaction जोड़े जाते हैं
Mouse से नियंत्रित अंतिम interaction
- अंतिम चरण में mouse coordinates को
u_mouse uniform के रूप में लेकर blob के अंदर की एक ball को user सीधे नियंत्रित कर सकता है
uniform vec2 u_mouse;
- mouse coordinates को ball center array में जोड़ने से एक पंक्ति में interaction आ जाता है
vec2 centers[5] = vec2[5](c1,c2,c3,c4,u_mouse);
- अंतिम shader canvas coordinates के अनुसार mouse y-axis को flip करता है, और 4 moving centers तथा mouse center को साथ में blend करता है
- color को
mix(colorA, colorB, percent) का कई बार उपयोग करके संयोजित किया जाता है
percent अगर boolean की तरह इस्तेमाल हो तो यह if/else जैसा काम करता है
metaball value, center point distance, shine, और membrane की गणना अंतिम color में दिखाई देती है
- यही सिद्धांत समझ लेने पर Blender’s shader nodes या Unity’s Shader Graph जैसे node-based shader editor के अंदरूनी काम को भी बेहतर समझा जा सकता है
आगे देखने लायक सामग्री
1 टिप्पणियां
Hacker News की राय
आखिरकार इंटरनेट पर लिखने और खुद को सामने रखने की हिम्मत जुटाई। काफ़ी समय से shaders सीखना चाहता था, और लगा कि सीखने की प्रक्रिया को document करके दूसरों के साथ शेयर करना अच्छा रहेगा
ईमानदार blogging सर्वशक्तिमान algorithm की छाया में अदृश्य रूप से मरती जा रही है, और इंटरनेट ऐसा लग रहा है जैसे Star Wars Episode IV के बीच तक पहुँच चुका हो। feedback बस एक है: धन्यवाद, और ज़्यादा लिखिए
अगर देखना चाहते हैं कि shader से उस्ताद लोग क्या कर सकते हैं, तो मैं Inigo Quilez और उनकी shader art की सिफारिश करूँगा: https://www.youtube.com/watch?v=BFld4EBO2RE
जोड़ा गया: मैंने नहीं देखा था कि आप ही लेखक हैं। लेख बहुत अच्छा बना है, और मैं shader coding art को अधिक approachable और interactive तरीके से समझाने वाला tutorial ढूँढ रहा था
smoothstep(0.0f, 0.01f, dist);जैसे constants अंदाज़े से चुनने के बजायsmoothstep(fwidth(dist), -fwidth(dist), dist);इस्तेमाल करेंलेख काफ़ी अच्छा है, लेकिन shaders की मूल समस्या को बहुत हल्के में पार कर जाता है।
Shaders ज़्यादातर programs और applications के लिए अनचाहा सिरदर्द हैं। 3D को triangles पसंद हैं और GPU भी उस abstraction पर अच्छी तरह fit बैठता है, और shaders उन triangles पर interpolation करने में उपयोगी हैं
लेकिन 3D के अलावा लगभग हर जगह triangles ख़ास अच्छे नहीं हैं। 2D rendering paths चाहती है, font rendering paths या pixmaps चाहती है, GUI में paths और pixmaps कहीं बेहतर fit होते हैं, compositor pixmaps चाहता है, और video decoder pixmaps और parallel rendering चाहता है
non-3D पक्ष जो चाहता है वह rectangular pixmaps और उन pixmaps पर direct computational access है, लेकिन GPU इसे बहुत पसंद नहीं करता और shaders भी इसमें अच्छी तरह fit नहीं होते
यह सही है कि high level पर यह मनचाहा shape नहीं है, लेकिन higher-level abstraction लागू करने के आधार के रूप में यह काफ़ी अच्छा है। hardware में इसे सीधे support किया जा सकता है, लेकिन फायदा स्पष्ट नहीं है। कोई यह शिकायत नहीं करता कि CPU architecture level पर
forloops support नहीं करतादोबारा पढ़ने पर समझ नहीं आ रहा कि GPU को लेकर असल समस्या क्या बताई जा रही है। vertex processing pipeline को पूरी तरह ignore करके बस एक full-screen rectangle draw करें या compute shader इस्तेमाल करें, और GPU इसे बहुत अच्छे से handle करता है। link किया गया लेख भी इसी तरह की चीज़ों की बात कर रहा है
कम-से-कम 2000s में सोचने का तरीका यही था। software के और complex होने के साथ GPU मनचाहा optimization बन सकता है, लेकिन GPU pipeline हमेशा कठोर और बंद रही है, और single-core से multi-core programming की ओर जाते समय का paradigm अक्सर अलग algorithms मांगता है
general-purpose GPU programming आपको triangles से बंधे रहने से छुड़ाती है, लेकिन उस parallelism का फायदा लेने के लिए फिर भी पूरी तरह अलग approach चाहिए
शानदार! हाल ही में मैं SDF rabbit hole में थोड़ा उतर रहा था। iq site का link देखकर अच्छा लगा, वहाँ की material सच में बहुत अच्छी है
उनके “happy bouncing” shader को link किए बिना नहीं रह सकता। निजी तौर पर मुझे यह अद्भुत लगता है: https://www.shadertoy.com/view/3lsSzf
इसे बनाने की प्रक्रिया पर 6 घंटे का YouTube video भी है। करीब 500 lines का घना code है
इस विषय में दिलचस्पी लेने की कई बार कोशिश की, लेकिन कोई approachable entry point नहीं मिला था; इस introduction article में वह मिल गया। यह सच में मजेदार और playful तरीका है, और अगला लेख जल्दी पढ़ना चाहता हूँ
छोटी-सी nitpick है, लेकिन cel shading का ज़िक्र करते हुए
cellलिखा गया है। यह term hand-drawn animation में इस्तेमाल होने वाले cel और उस shading के quantized tones से आया हैसच में अच्छा है। एक artist से programmer बने व्यक्ति के तौर पर कभी-कभी graphics programming में गहराई से उतरने का मन होता है
कुछ बहुत basic shaders लिखे हैं, लेकिन जैसे ही math आने लगता है—असल में काफ़ी शुरू में ही—दीवार से टकरा जाता हूँ। मैंने computer science नहीं, fine arts पढ़ी है, इसलिए math skills लगभग नहीं के बराबर हैं
बहरहाल, अच्छा लिखा है और लेख पसंद आया
बेहतरीन introductory article है और उम्मीद है यह जारी रहेगा। ऐसे लेख शानदार introduction से शुरू होकर बहुत बार बीच में रुक जाते हैं
मैंने shaders पर कभी काम नहीं किया है, इसलिए यह बहुत बुनियादी सवाल हो सकता है। क्या game के एक frame में मूल रूप से shader ही सब कुछ draw करता है?
या फिर triangle, square, circle जैसी basic shapes होती हैं, और shader उनके ऊपर shadow या edge correction जैसी चीज़ें जोड़ता है?
उदाहरणों को देखकर लगता है कि scene में किसी भी object को draw करने वाला shader बनाया जा सकता है, और फिर दूसरे shaders को combine करके shadow और lighting जैसी चीज़ें पाई जाती हैं। मेरे बहुत सीमित drawing अनुभव में मैंने shapes draw की थीं, shaders से draw नहीं किया था। मैं अब तक यही सोचता रहा कि shader object को खुद draw नहीं करता
GPU तीन vertices और हर vertex से जुड़े normal जैसे data से defined triangle जैसी abstract vector shape को, output buffer में उस shape द्वारा cover किए गए हर pixel के लिए एक, और multisampling हो तो उससे भी ज़्यादा, fragments की stream में बदल देता है। यह हिस्सा पूरा hardware handle करता है
fragment pixel coordinates और user-provided data से बना होता है। वह data constant uniform हो सकता है, या पहले बताए गए vertex data का triangle face के पूरे हिस्से में interpolated varying हो सकता है। यह interpolation भी hardware करता है और programmable नहीं होता
fragment shader fragment को input के रूप में लेकर उस data के आधार पर color calculate करता है, और कुछ और stages के बाद उसे screen या offscreen buffer के संबंधित pixel color के रूप में output करता है। यह solid color भी हो सकता है और complex lighting calculation भी
GPU rendering में यह सब बड़े पैमाने पर parallel तरीके से होता है, जिससे बहुत सारे fragments एक साथ process होते हैं। shader pure और stateless function होता है, इसलिए वह सिर्फ input तक access कर सकता है, और उसके effects भी color और depth value जैसी कुछ चीज़ें return करने तक सीमित होते हैं
संक्षेप में, GPU hardware यह calculate करता है कि हर triangle को draw करने के लिए कौन से pixels भरने होंगे, और fragment shader उन हर pixel की color value तय करता है
fragment shader के scene “के ऊपर” draw करने से पहले vertex, tessellation जैसे दूसरे stages basic shapes draw करते हैं
fragment shader के बारे में भी ऊपर समझाई गई बातों से कहीं ज़्यादा कुछ है। उदाहरण के लिए deferred rendering[2], और वह भी अपने आप में बड़ा topic है
1: https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipe...
2: https://learnopengl.com/Advanced-Lighting/Deferred-Shading
graphics pipeline fixed-function hardware stages और programmable stages का मिश्रण होती है। high level पर: 1) GPU CPU से 3D triangles का set लेता है, 2) vertex shader 3D triangle vertices को pixel coordinates वाले 2D triangle vertices में transform करके flat बनाता है, 3) GPU 2D triangles को rasterize करके ठीक-ठीक तय करता है कि कौन से pixels triangle से cover होते हैं, 4) cover हुए हर pixel के लिए pixel shader run होकर pixel color तय करता है, और 5) resulting pixel color frame buffer में store होता है। इस दौरान वह existing color के साथ blend भी हो सकता है
यह pipeline अलग-अलग triangle mesh और shaders के साथ कई बार repeat होती है, जब तक पूरा frame draw नहीं हो जाता
color, shadow, shading, image effects, general image processing जैसे काम कई data arrays को combine करने वाली parallel computation से होते हैं। इनमें vertices और उनके attributes, source textures, precomputed functions, target textures, buffers आदि शामिल होते हैं
उदाहरण के लिए light और shadows पाने के लिए shader को spotlight की position और direction जैसी चीज़ों तक, शायद global variables के रूप में, access चाहिए होता है। composite lighting अक्सर कई shader passes combine करके मिलती है। जैसे global lighting के लिए एक base pass और हर light के लिए एक pass, और हर pass literally light add करता है
अब जिन pixels तक light source नहीं पहुँचता, यानी shadows में, light add न करने के लिए सबसे common technique Z-buffer नाम की चीज़ का उपयोग है। यह असल में floating-point texture होता है। चूँकि हमें जानना होता है कि scene की हर light कहाँ तक पहुँचती है, सारी lighting apply करने से पहले scene की सारी solid geometry को combine करने वाला एक single shader pass set किया जाता है, light की position और direction को camera transform की तरह use किया जाता है, और एक special shader use होता है जो सिर्फ object तक की दूरी को Z-buffer में लिखता है
बाद में जब भी जानना हो कि space में किसी point तक light पहुँचती है या नहीं, तो थोड़ी geometry calculation के बाद इस Z-buffer को sample किया जाता है, और उस direction में stored value की तुलना उस point की दूरी से की जाती है। इसमें bugs बहुत हो सकते हैं और precision errors भी common हैं। अच्छा engine यह काम पहले से कर देता है, लेकिन आपको उस process को modify करने देता है
बाकी सब इसी topic की variations हैं। deferred rendering में color के बजाय data को intermediate textures में render किया जाता है और फिर बाद में process करके color पाया जाता है। blur effect render texture पर 2D convolution, जैसे Gaussian kernel, से किया जाता है। tessellation shader vertex shader में नई geometry generate करने से जुड़ा है। text drawing भी font atlas और छोटे rectangles के जरिए होती है
इसलिए talented artists fragment shader के अंदर limits push करते हैं और performance trade-offs से जूझते हैं
fragment shader ज़्यादा आम तौर पर full-screen filter effects, जैसे color correction आदि के लिए इस्तेमाल होता है
shaders basic objects के texture और material बनाने में भी इस्तेमाल होते हैं। material artists अक्सर shader math से textures generate करते हैं
कई visual effects shaders के creative use से बनाए जाते हैं
shaders GPU पर parallel, wave जैसी शैली में execute होते हैं। बहुत सारे threads एक wave के भीतर उसी data पर run होते हैं
कुछ मामलों में shader CPU के branching code से कहीं तेज़ होता है। shader कुछ rendering data तक भी ज़्यादा आसानी से access कर सकता है
इसलिए यह creative special effects बनाने के लिए अच्छी जगह है। गेम्स में जिन objects की surface detail ज्यादा होती है, वे उस detail को shader में ले जाने के आम candidates होते हैं। समुद्र की surface, tessellation mesh वगैरह इसके उदाहरण हैं, और GPU शक्तिशाली व flexible होने की वजह से इसके कई और उपयोग भी हैं।
अगर किसी को image टिमटिमाते noise जैसी दिख रही हो, तो इसे browser में image copy करके कहीं और paste करने से ठीक किया। तब image ठीक से दिखती है
यह Imgur link है। पहली image browser में दिखने वाली screen का screenshot है, और बाकी Imgur में paste करने के बाद की असली images हैं
https://imgur.com/a/F4203rz
shaders को लेकर मेरी समझ यह है।
CPU पर line draw करते समय, यह ऐसा function होता है जो point A और point B के बीच हर pixel पर iterate करके एक-एक pixel क्रम से draw करता है। line में जितने pixels हैं, ठीक उतने ही steps होते हैं और यह एक बार execute होता है
GPU पर line draw करते समय, यह ऐसा function होता है जो check करता है कि संबंधित pixel line पर है या नहीं, और अगर है तो उसे draw करता है। यह screen के सभी pixels पर एक साथ execute होता है, जिसमें line से काफी दूर वाले pixels भी शामिल होते हैं
क्या यह सही है?
दूसरी बात, GPU को पूरी screen के लिए pixel shader execute करने की जरूरत भी नहीं होती। triangles का इस्तेमाल करके सिर्फ मनचाही arbitrary shape पर shader execute कराया जा सकता है। इसलिए line को efficiently draw करने का तरीका यह है कि इच्छित line geometry से मेल खाते दो triangles GPU को भेजे जाएं, और pixel shader सिर्फ उन pixels पर execute हो जहां वे triangles overlap करते हैं। यह कहीं ज्यादा efficient है