مقدمه ای بر شیدرها

معرفی

من قبلاً به شما مقدمه ای برای Three.js داده ام. اگر آن را نخوانده‌اید، شاید بخواهید، زیرا این پایه‌ای است که من در طول این مقاله بر روی آن خواهم ساخت.

کاری که من می‌خواهم انجام دهم بحث در مورد سایه‌زنان است. WebGL فوق‌العاده است و همانطور که قبلاً گفته‌ام Three.js (و کتابخانه‌های دیگر) کار خارق‌العاده‌ای برای حذف مشکلات برای شما انجام می‌دهند. اما مواقعی وجود خواهد داشت که می خواهید به یک جلوه خاص دست پیدا کنید، یا می خواهید کمی عمیق تر در مورد نحوه ظاهر شدن آن چیزهای شگفت انگیز بر روی صفحه نمایش خود کاوش کنید و سایه بان ها تقریباً به طور قطع بخشی از این معادله خواهند بود. همچنین اگر شما هم مانند من هستید، ممکن است بخواهید از موارد اولیه در آخرین آموزش به چیز کمی پیچیده تر بروید. من بر این اساس کار می‌کنم که شما از Three.js استفاده می‌کنید، زیرا این کار بسیاری از کارها را برای ما انجام می‌دهد. همچنین از قبل می گویم که در ابتدا زمینه را برای سایه بان ها توضیح خواهم داد، و قسمت آخر این آموزش جایی است که به قلمرو کمی پیشرفته تر خواهیم رسید. دلیل این امر این است که سایه بان ها در نگاه اول غیرعادی هستند و نیاز به توضیح کمی دارند.

1. دو سایه بان ما

WebGL استفاده از Fixed Pipeline را ارائه نمی دهد، که روشی کوتاه برای بیان این است که هیچ وسیله ای برای رندر کردن چیزهای خود در خارج از جعبه به شما نمی دهد. با این حال، چیزی که ارائه می دهد، خط لوله قابل برنامه ریزی است که قدرتمندتر است اما درک و استفاده از آن دشوارتر است. به طور خلاصه، برنامه‌پذیر Pipeline به این معنی است که شما به عنوان برنامه‌نویس مسئولیت نمایش رئوس و غیره را بر روی صفحه نمایش می‌دهید. سایه بان ها بخشی از این خط لوله هستند و دو نوع از آنها وجود دارد:

  1. سایه زن های راس
  2. شیدرهای قطعه

هر دوی آنها، مطمئنم که شما موافق خواهید بود، به خودی خود هیچ معنایی ندارند. چیزی که باید در مورد آنها بدانید این است که هر دو به طور کامل بر روی GPU کارت گرافیک شما کار می کنند. این به این معنی است که ما می‌خواهیم تمام آنچه را که می‌توانیم برای آنها بارگذاری کنیم و CPU خود را به انجام کارهای دیگر بسپاریم. یک GPU مدرن به شدت برای عملکردهایی که سایه بان ها نیاز دارند بهینه شده است، بنابراین استفاده از آن عالی است.

2. Vertex Shaders

یک شکل ابتدایی استاندارد مانند یک کره بگیرید. از رئوس تشکیل شده است، درست است؟ یک سایه زن رأس به هر یک از این رئوس به نوبه خود داده می شود و می تواند با آنها درگیر شود. این به سایه‌زن رأس بستگی دارد که در واقع با هر کدام چه می‌کند، اما یک مسئولیت دارد: باید در نقطه‌ای چیزی به نام gl_Position ، یک بردار شناور ۴ بعدی، که موقعیت نهایی راس روی صفحه است، تنظیم کند. به خودی خود این فرآیند بسیار جالبی است، زیرا ما در واقع در مورد قرار دادن یک موقعیت سه بعدی (یک راس با x,y,z) بر روی یک صفحه دوبعدی یا پیش بینی شده صحبت می کنیم. خوشبختانه اگر از چیزی مانند Three.js استفاده می‌کنیم، روشی مختصر برای تنظیم gl_Position بدون سنگین شدن چیزها خواهیم داشت.

3. Fragment Shaders

بنابراین ما شیء خود را با رئوس آن داریم و آنها را به صفحه دوبعدی نمایش داده‌ایم، اما رنگ‌هایی که استفاده می‌کنیم چطور؟ تکسچرینگ و نورپردازی چطور؟ این دقیقا همان چیزی است که shader قطعه برای آن وجود دارد. بسیار شبیه سایه‌زن رأس، سایه‌زن قطعه نیز تنها یک کار ضروری دارد: باید متغیر gl_FragColor ، یک بردار شناور 4 بعدی دیگر، که رنگ نهایی قطعه ما است را تنظیم یا کنار بگذارد. اما قطعه چیست؟ به سه راس فکر کنید که یک مثلث را تشکیل می دهند. هر پیکسل در آن مثلث باید کشیده شود. یک قطعه داده ای است که توسط آن سه راس برای رسم هر پیکسل در آن مثلث ارائه می شود. به همین دلیل قطعات مقادیر درون یابی را از رئوس تشکیل دهنده خود دریافت می کنند. اگر یک راس قرمز رنگ باشد و همسایه آن آبی باشد، مقادیر رنگ را از قرمز، از بنفش، به آبی می بینیم.

4. متغیرهای سایه زن

هنگامی که در مورد متغیرها صحبت می کنید، سه اعلان وجود دارد که می توانید بیان کنید: Uniforms ، Attributes و Varyings . وقتی برای اولین بار نام آن سه را شنیدم، بسیار گیج شدم زیرا با هیچ چیز دیگری که تا به حال با آنها کار کرده بودم مطابقت ندارند. اما در اینجا این است که چگونه می توانید در مورد آنها فکر کنید:

  1. یونیفرم ها هم به سایه زن های راس و هم به سایه زن های قطعه ارسال می شوند و حاوی مقادیری هستند که در کل فریم رندر شده یکسان می مانند. یک مثال خوب از این ممکن است موقعیت یک نور باشد.

  2. ویژگی ها مقادیری هستند که برای رئوس جداگانه اعمال می شوند. ویژگی ها فقط برای سایه زن راس در دسترس هستند. این می تواند چیزی شبیه به هر رأس دارای رنگ مجزا باشد. ویژگی ها با رئوس رابطه یک به یک دارند.

  3. متغیرهای s متغیرهایی هستند که در سایه‌زن رأس اعلام شده‌اند که می‌خواهیم با سایه‌زن قطعه به اشتراک بگذاریم. برای انجام این کار، مطمئن می شویم که یک متغیر متغیر از همان نوع و نام را هم در سایه زن رأس و هم در سایه زن قطعه اعلام می کنیم. استفاده کلاسیک از این یک راس طبیعی است زیرا می توان از آن در محاسبات روشنایی استفاده کرد.

بعداً از هر سه نوع استفاده خواهیم کرد تا بتوانید در مورد نحوه اعمال واقعی آنها احساس کنید.

اکنون ما در مورد سایه‌زن‌های رأس و سایه‌زن‌های قطعه و انواع متغیرهایی که با آن‌ها سروکار دارند صحبت کرده‌ایم، اکنون ارزش دارد ساده‌ترین سایه‌زن‌هایی را که می‌توانیم ایجاد کنیم، نگاه کنیم.

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);
}   

و در اینجا برای shader قطعه یکسان است:

/**
* 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 برای ما ارسال می‌شود. این دو یونیفرم ماتریس های 4 بعدی هستند که به آنها ماتریس Model-View و Matrix Projection گفته می شود. به شدت نیازی به دانستن این که اینها دقیقاً چگونه کار می کنند، ندارید، اگرچه همیشه بهتر است درک کنید که اگر می توانید کارها را چگونه انجام می دهند. نسخه کوتاه این است که چگونه موقعیت سه بعدی راس در واقع به موقعیت دو بعدی نهایی روی صفحه نمایش داده می شود.

من در واقع آنها را از قطعه بالا حذف کردم زیرا 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 سایه‌بان‌های شما را که به مشی متصل شده‌اند را کامپایل و اجرا می‌کند. خیلی ساده تر از این نمی شود واقعا. خوب احتمالاً اینطور است، اما ما در مورد اجرای سه بعدی در مرورگر شما صحبت می کنیم، بنابراین فکر می کنم شما انتظار پیچیدگی خاصی را دارید.

ما در واقع می‌توانیم دو ویژگی دیگر به MeshShaderMaterial خود اضافه کنیم: لباس‌ها و ویژگی‌ها. آنها می توانند هر دو بردار، اعداد صحیح یا شناور بگیرند، اما همانطور که قبلاً اشاره کردم، یکنواخت ها برای کل قاب یکسان هستند، یعنی برای همه رئوس، بنابراین تمایل دارند مقادیر واحد باشند. با این حال، ویژگی ها متغیرهای هر راس هستند، بنابراین انتظار می رود که آنها یک آرایه باشند. باید یک رابطه یک به یک بین تعداد مقادیر در آرایه ویژگی ها و تعداد رئوس در مش وجود داشته باشد.

7. مراحل بعدی

اکنون می‌خواهیم کمی زمان را صرف اضافه کردن یک حلقه انیمیشن، ویژگی‌های راس و یک فرم کنیم. ما همچنین یک متغیر متغیر اضافه می کنیم تا سایه زن رأس بتواند مقداری داده را به shader قطعه ارسال کند. نتیجه نهایی این است که کره ما که صورتی بود به نظر می رسد که از بالا و کنار روشن می شود و می تپد. به نوعی سهل‌انگیز است، اما امیدواریم که شما را به درک خوبی از سه نوع متغیر و همچنین نحوه ارتباط آنها با یکدیگر و هندسه زیرین هدایت کند.

8. نور جعلی

بیایید رنگ‌آمیزی را به‌روزرسانی کنیم تا یک شیء رنگی صاف نباشد. ما می‌توانیم نگاهی به نحوه عملکرد Three.js با نورپردازی بیندازیم، اما همانطور که مطمئن هستم می‌توانید درک کنید که پیچیده‌تر از آن چیزی است که در حال حاضر نیاز داریم، بنابراین ما آن را جعل می‌کنیم. شما باید کاملاً از طریق سایه‌بان‌های خارق‌العاده که بخشی از Three.js هستند و همچنین آنهایی که از پروژه شگفت‌انگیز WebGL اخیر توسط کریس میلک و گوگل، رم هستند، نگاه کنید. بازگشت به سایه بان ما. ما Vertex Shader خود را به‌روزرسانی می‌کنیم تا هر راس عادی را به Fragment Shader ارائه دهیم. ما این کار را با یک متغیر انجام می دهیم:

// 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);
}

و در Fragment Shader می‌خواهیم همان نام متغیر را تنظیم کنیم و سپس از حاصل ضرب نقطه‌ای راس نرمال با بردار استفاده کنیم که نوری را نشان می‌دهد که از بالا و سمت راست کره می‌تابد. نتیجه خالص این به ما جلوه ای شبیه به یک نور جهت دار در یک بسته سه بعدی می دهد.

// 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 تنظیم نشده است، بنابراین سایه‌زن به‌جای آن از مقدار صفر استفاده می‌کند. در حال حاضر به نوعی مانند یک مکان نگهدار است. در یک ثانیه ما ویژگی را به 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 اضافه می کنیم. ابتدا 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 به چیز دیگری نیستیم.

در جاوا اسکریپت ما اکنون باید فراخوانی رندر را در یک تابع جمع بندی کنیم و سپس از 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. نتیجه گیری

و بس! اکنون می توانید ببینید که به شیوه ای عجیب و غریب (و کمی تپنده) متحرک است.

موارد بسیار بیشتری وجود دارد که می‌توانیم در مورد سایه‌زن‌ها به‌عنوان موضوع پوشش دهیم، اما امیدوارم این مقدمه برای شما مفید بوده باشد. اکنون باید بتوانید هنگام دیدن سایه بان ها را درک کنید و همچنین اعتماد به نفس ایجاد چند شیدر شگفت انگیز از خودتان داشته باشید!