تقریب کره، پیادهسازی روشنایی، نورپردازی قطعهای و نورپردازی سراسری
ادامهٔ Phong Shading نیاز دارد مدل نورپردازی برای هر Fragment اعمال شود؛ از همین رو این روش را Per-Fragment Shading نیز مینامند. آن را با Fragment Shader پیادهسازی خواهیم کرد.
5.6 تقریب کره با تقسیمبندی بازگشتی
از کره به عنوان سطح خمیدهٔ نمونه برای توضیح محاسبات Shading استفاده کردیم. OpenGL خود Primitive کره ندارد؛ بنابراین آن را با مثلثها و تکنیکی به نام Recursive Subdivision تقریب میزنیم، همان ایدهای که در فصل ۲ برای واشر سیرپینسکی معرفی شد. این تکنیک روشی قدرتمند برای تقریب منحنیها و سطوح تا هر دقت مطلوب است و مدل کره بستری مناسب برای آزمایش اثر پارامترهای Shading و میزان ریزشدگی Polygonها فراهم میکند.
از یک چهاروجهی منظم شروع میکنیم، هرچند هر چندوجهی منظم با Facetهای قابل مثلثبندی نیز قابل استفاده است.² چهاروجهی منظم از چهار مثلث متساویالاضلاع و چهار رأس تشکیل میشود. رأسهای انتخابی همگی روی کرهٔ واحدِ مرکز مبدأ قرار دارند:
(0,0,1),\quad (0,2\sqrt2/3,-1/3),
(-\sqrt6/3,-\sqrt2/3,-1/3),\quad(\sqrt6/3,-\sqrt2/3,-1/3).
در کد:
point4 v[4]= {vec4(0.0, 0.0, 1.0, 1.0),
vec4(0.0, 0.942809, -0.333333, 1.0),
vec4(-0.816497, -0.471405, -0.333333, 1.0),
vec4(0.816497, -0.471405, -0.333333, 1.0)};
دادهٔ مثلثها در آرایهٔ
قرار میگیرد و تابع triangle سه رأس را اضافه میکند:
static int k = 0;
void triangle(point4 a, point4 b, point4 c)
{
data[k] = a;
² بیست وجه مثلثی Icosahedron نقطهٔ شروع خوبی برای ساخت کره است.
k++;
data[k] = b;
k++;
data[k] = c;
k++;
}
چهار وجه چهاروجهی:
void tetrahedron()
{
triangle(v[0], v[1], v[2]);
triangle(v[3], v[2], v[1]);
triangle(v[0], v[3], v[1]);
triangle(v[0], v[2], v[3]);
}
ترتیب رأسها از قانون دست راست پیروی میکند و بعدها تبدیل نمایش Wireframe به Polygonهای Shadeشده را ساده میسازد.
شکل 5.32 ــ چهاروجهی. این شکل یک چندوجهی منظم ساده اما تقریب ضعیفی از کره است.
برای تقریب بهتر، هر Facet را به مثلثهای کوچکتر تقسیم میکنیم. سه راه معمول در شکل 5.33 آمده است: نیمساز زاویهها و ساخت سه مثلث؛ اتصال مرکز جرم به سه رأس و ساخت سه مثلث؛ یا اتصال وسط ضلعها و ساخت چهار مثلث متساویالاضلاع. دو روش نخست متساویالاضلاعبودن مثلثها را حفظ نمیکنند، پس مانند ساخت سیرپینسکی از روش سوم استفاده میکنیم.
شکل 5.33 ــ تقسیم مثلث: (الف) نیمساز زاویهها، (ب) مرکز جرم، (ج) نیمکردن ضلعها.
پس از تقسیم، رأسهای جدید هنوز در صفحهٔ مثلث اصلیاند. برای نزدیکشدن به کره باید رأسهای میانی را Normalize کنیم و روی کرهٔ واحد ببریم.
برای مثلث با رأسهای a,b,c:
point4 v1, v2, v3;
v1 = normalize(a + b);
v2 = normalize(a + c);
v3 = normalize(b + c);
triangle(a, v2, v1);
triangle(c, v3, v2);
triangle(b, v1, v3);
triangle(v1, v2, v3);
اما میخواهیم این تقسیم (n) بار تکرار شود. tetrahedron عمق بازگشت را میگیرد:
void tetrahedron(int n)
{
divide_triangle(v[0], v[1], v[2], n);
divide_triangle(v[3], v[2], v[1], n);
divide_triangle(v[0], v[3], v[1], n);
divide_triangle(v[0], v[2], v[3], n);
}
و تابع بازگشتی تا زمانی که n>0 است چهار زیرمثلث میسازد؛ در سطح صفر خود مثلث را خروجی میدهد:
void divide_triangle(point4 a, point4 b, point4 c, int n)
{
point4 v1, v2, v3;
if (n>0)
{
v1 = normalize(a + b);
v2 = normalize(a + c);
v3 = normalize(b + c);
divide_triangle(a, v2, v1, n-1);
divide_triangle(c, v3, v2, n-1);
divide_triangle(b, v1, v3, n-1);
divide_triangle(v1, v2, v3, n-1);
}
else triangle(a, b, c);
}
شکل 5.34 ــ تقریبهای کره با Subdivision. اکنون میتوان به مدل تقریبزده نور و Shading افزود.
5.7 مشخصکردن پارامترهای نورپردازی
برای سالها مدل Blinn-Phong مدل استاندارد گرافیک رایانهای بود و حتی در سختافزار و Fixed-Function Pipeline قدیمی OpenGL پیادهسازی میشد. با Shaderهای برنامهپذیر اکنون میتوان مدلهای دیگر را نیز بدون افت بنیادی کارایی پیاده کرد و محل اجرای مدل ــ برنامه، Vertex Shader یا Fragment Shader ــ را انتخاب نمود. در هر حالت باید مجموعهای از پارامترهای نور و ماده تعریف و در محل مناسب استفاده یا به Shader ارسال شوند.
5.7.1 منابع نور
چهار نوع منبع Ambient، Point، Spotlight و Distant را معرفی کردیم. چون Spotlight و Distant را میتوان از Point Source توسعه داد، تمرکز اصلی بر Point Source و Ambient است. Spotlight با محدودکردن جهتهای تابش و اعمال Profile شدت به Point Source ساخته میشود. Distant Source با بردن مکان منبع به بینهایت به منبعی تبدیل میشود که فقط جهت آن اهمیت دارد؛ همان استدلالی که موازیبودن projection را حد پرسپکتیو میدانست.
Ambient Light نیز در نهایت به منابع محیط وابسته است. در اتاق بسته با یک منبع سفید، خاموشبودن منبع یعنی نبود نور. با روشنشدن آن، بخشی از نور مستقیماً سطوح را روشن و در Diffuse/Specular دیده میشود و بخشی پس از بازتابهای متعدد تقریباً به صورت یکنواخت به نقاط مختلف میرسد؛ همین سهم را Ambient مینامیم. رنگ Ambient علاوه بر رنگ منبع به خواص سطوح اتاق وابسته است؛ مثلاً دیوارهای قرمز Ambient غالباً قرمز میسازند. بنابراین Ambient را نیز بخشی از مشخصات منبع در نظر میگیریم.
برای هر منبع باید رنگ و مکان یا جهت آن مشخص شود. رنگ سه مؤلفهٔ Ambient/Diffuse/Specular دارد:
color4 light_diffuse, light_specular, light_ambient;
مکان:
Point Source در مختصات همگن:
point4 light_position = vec4(1.0, 2.0, 3.0, 1.0);
اگر مؤلفهٔ چهارم صفر شود:
point4 light_position = vec4(1.0, 2.0, 3.0, 0.0);
منبع به Directional Source در جهت ((1,2,3)) تبدیل میشود.
برای منبع مکانی میتوان افت فاصله را با
f(d)=\frac{1}{a+bd+cd^2}
مدل کرد:
float attenuation_constant, attenuation_linear, attenuation_quadratic;
این مقادیر میتوانند در برنامه استفاده یا به صورت Uniform به Shader فرستاده شوند.
برای تبدیل منبع مکانی به Spotlight نیز جهت، زاویهٔ مخروط (cutoff) و سرعت افت شدت (spotlight exponent) را مشخص میکنیم.
5.7.2 مواد
خواص ماده باید با منابع و مدل بازتاب هماهنگ باشند. در صورت نیاز میتوان برای وجه جلو و پشت خواص متفاوت تعریف کرد.
برای مثال ضرایب بازتاب Ambient/Diffuse/Specular هر سه رنگ را میتوان چنین تعریف کرد:
color3 ambient = color3(0.2, 0.2, 0.2);
color3 diffuse = color3(1.0, 0.8, 0.0);
color3 specular = color3(1.0, 1.0, 1.0);
یا برای سطح کدر با RGBA:
color4 ambient = color4(0.2, 0.2, 0.2, 1.0);
color4 diffuse = color4(1.0, 0.8, 0.0, 1.0);
color4 specular = color4(1.0, 1.0, 1.0, 1.0);
در این مثال Ambient سفید کم، Diffuse زرد و Specular سفید تعریف شده است. علاوه بر آن باید Shininess مشخص شود:
برای پشت سطح:
color4 back_ambient, back_diffuse, back_specular;
گاهی خود منبع نور در حجم دید و قابل مشاهده است، مانند ماه در صحنهٔ شب. اگر آن را با Geometry معمولی رندر کنیم، نباید رنگش تحت تأثیر منابع دیگر باشد. برای این منظور Emission یا گسیل خودبهخودی ماده را اضافه میکنیم؛ این مؤلفه از منابع دیگر تأثیر نمیگیرد و به سطوح دیگر نیز نور نمیرساند، فقط یک رنگ ثابت به سطح اضافه میکند:
color4 emission = color4(0.0, 0.3, 0.3, 1.0);
برای مدیریت مواد بهتر است struct یا Class تعریف کنیم:
typedef struct materialStruct {
color4 ambient;
color4 diffuse;
color4 specular;
color4 emission;
float shininess;
} materialStruct;
سپس مادهها با یک شیء قابل انتخاب میشوند.
materialStruct brassMaterials =
{
{0.33, 0.22, 0.03, 1.0},
{0.78, 0.57, 0.11, 1.0},
{0.99, 0.91, 0.81, 1.0},
{0.0, 0.0, 0.0, 1.0},
27.8
};
currentMaterial = &brassMaterials;
5.8 پیادهسازی یک مدل نورپردازی
تا اینجا فقط پارامترها را معرفی کردهایم. اکنون یک نسخهٔ ساده از Blinn-Phong با یک Point Source میسازیم. چون نور چند منبع جمعپذیر است، محاسبه را میتوان برای هر منبع تکرار و نتیجهها را جمع کرد. سه محل اجرای اصلی وجود دارد: برنامهٔ کاربردی، Vertex Shader یا Fragment Shader. مدل پایه میتواند یکسان باشد، اما کارایی و ظاهر خروجی به محل محاسبه وابسته است.
5.8.1 اجرای مدل در برنامهٔ کاربردی
دو روش اصلی برای رنگ Polygonها داشتیم: ارسال یک Uniform Color برای کل Polygon، یا ارسال Color به عنوان Vertex Attribute و Interpolate شدن آن توسط Rasterizer. هر دو برای Lighting قابل استفادهاند. در Flat Shading مدل یک بار در هر Polygon اجرا و یک رنگ برای همهٔ Fragmentها استفاده میشود. در Interpolative Shading مدل در هر رأس اجرا میشود و Rasterizer رنگهای رأس را Interpolate میکند.
برای یک Point Source فرض کنید:
color4 light_ambient, light_diffuse, light_specular;
point4 light_position;
و ماده:
color4 reflect_ambient, reflect_diffuse, reflect_specular;
رنگ خروجی جمع سه سهم است:
color4 color_out, ambient, diffuse, specular;
color_out = ambient + diffuse + specular;
محصول مؤلفهبهمؤلفهٔ رنگ منبع و بازتاب ماده را میتوان با تابع زیر ساخت:
vec4 product(vec4 a, vec4 b)
{
return vec4(a[0]*b[0], a[1]*b[1], a[2]*b[2], a[3]*b[3]);
}
Ambient:
ambient = product(light_ambient, reflect_ambient);
برای Diffuse به Normal نیاز داریم. سه رأس v0,v1,v2 صفحه و Normal واحد را تعیین میکنند:
vec4 n = normalize(cross(v1-v0, v2-v1));
جهت Normal به ترتیب رأسها و قانون دست راست وابسته است.
چهار حالت مهم برای Diffuse داریم: Flat با منبع دور، Interpolative با منبع دور، Flat با منبع نزدیک، Interpolative با منبع نزدیک. برای منبع دور، جهت منبع در همهٔ نقاط مثلث یکسان است و Diffuse فقط یک بار محاسبه میشود:
color4 diffuse = product(light_diffuse, reflect_ambient)
*dot(n, normalize(light_position));
چون Dot Product منفی نباید نور منفی ایجاد کند:
float d = dot(n, normalize(light_position));
if (d>0) diffuse = product(light_diffuse, reflect_ambient)*d;
else diffuse = vec4(0.0, 0.0, 0.0, 1.0);
برای منبع دور، Diffuse در همهٔ رأسها برابر است و Flat/Interpolative از نظر Diffuse تفاوتی ندارند.
برای منبع نزدیک، جهت از هر نقطهٔ Polygon تا نور متفاوت است. در Flat Shading میتوان از مرکز مثلث استفاده کرد:
point4 v = (1.0/3.0)*(v0 + v1 + v2);
vector4 light_vector = light_position - v;
float d = dot(n, normalize(light_vector));
if (d>0) diffuse = product(light_diffuse, reflect_ambient)*d;
else diffuse = vec4(0.0, 0.0, 0.0, 1.0);
برای Specular به Halfway Vector نیاز داریم. برای منبع دور:
vec4 half = normalize(light_position + view_direction);
اگر دوربین پیشفرض در مبدأ باشد:
vec4 origin = vec4(0.0, 0.0, 0.0, 1.0);
vec4 view_direction = v - origin;
حتی با Normal ثابت مثلث، Halfway Vector در هر رأس متفاوت است؛ بنابراین Specular روی سطح تغییر میکند.
if (s>0.0)
specular = pow(s, material_shininess)
*product(light_specular, material_specular);
else
specular = vec4(0.0, 0.0, 0.0, 1.0);
5.8.2 کارایی
در صحنهٔ ثابت، Lighting یک بار محاسبه و Position/Color رأسها یک بار به GPU فرستاده میشود. اما در مکعب چرخان، با هر دوران Normal چهار وجه و موقعیت رأسها تغییر میکند؛ اگر محاسبات روی CPU باشند، باید Diffuse/Specular دوباره محاسبه و Position/Color دوباره به GPU منتقل شوند. برای دادهٔ بزرگ بسیار ناکارآمد است: هم CPU درگیر میشود و هم انتقال داده میتواند Bottleneck ایجاد کند. بنابراین تقریباً همیشه بهتر است Lighting در Shader انجام شود.
سادهسازیهای دیگری نیز ممکناند. اگر منبع یا Viewer نسبت به اندازهٔ Polygon بسیار دور باشند، جهتها در رأسهای مختلف تقریباً یکساناند. در این حالت Diffuse یا Specular را میتوان با محاسبات کمتر تقریب زد. «نزدیک/دور» هم به فاصله و هم اندازهٔ Polygon وابسته است.
در فصل ۳ دیدیم سطح Face جلو و پشت دارد. برای بیشتر اشیای بسته و محدب، مانند کره یا Parallelepiped، Viewer هرگز Back Face را نمیبیند؛ میتوان آنها را Cull کرد. اما اگر یک ضلع مکعب حذف شود یا کره برش بخورد، Back Face ممکن است قابل مشاهده باشد و باید بهدرستی Shade شود. ممکن است Back Face مادهای متفاوت از Front Face داشته باشد و در این صورت پارامترهای جدا لازم است.
شکل 5.35 ــ Shading اجسام محدب.
شکل 5.36 ــ Back Surfaceهای قابل مشاهده.
منابع نور نیز اشیای هندسی با Attributeهایی مانند Position هستند و تبدیلها میتوانند بر آنها اثر بگذارند. میتوان منبع را مستقیماً در جای مطلوب تعریف کرد یا در جای سادهای تعریف و با model-view منتقل کرد. قاعدهٔ بنیادی این است که رأسها با model-view فعال در زمان تعریفشان به مختصات چشم تبدیل میشوند. با انتخاب زمان مناسب تعریف منبع و اشیا میتوان منبع ثابت با اشیای متحرک، منبع متحرک با اشیای ثابت یا منبعی که همراه شیء حرکت میکند ساخت.
همچنین باید دستگاه مختصات محاسبات Lighting را انتخاب کنیم. فعلاً در مختصات شیء کار میکنیم؛ در بعضی شرایط Eye Coordinates کارآمدتر است. در بحث Texture Mapping حتی دستگاههای مختصات محلی نیز وارد میشوند.
5.8.3 نورپردازی در Vertex Shader
همانگونه که اجرای model-view در Vertex Shader معمولاً از اجرای آن روی CPU کارآمدتر است، Lighting نیز غالباً بهتر است در Shader انجام شود. سه گام داریم: انتخاب مدل نورپردازی، نوشتن Shader، و انتقال دادهٔ لازم با Uniformها و Vertex Attributeها.
برای Blinn-Phong بدون افت فاصله و با یک Point Source، Ambient/Diffuse/Specular و Position منبع Uniform هستند؛ خواص ماده نیز Uniform میشوند. Vertex Shader باید Position رأس در Clip Coordinates و Color رأس را خروجی دهد. بخش Position مانند فصلهای ۳ و ۴ است:
in vec4 vPosition;
uniform mat4 ModelView;
uniform mat4 Projection;
void main()
{
gl_Position = Projection*ModelView*vPosition;
}
رنگ خروجی:
out vec4 color;
vec4 ambient, diffuse, specular;
color = ambient + diffuse + specular;
برای کاهش Uniformها، حاصلضرب رنگ منبع و ضریب بازتاب ماده را در برنامه محاسبه و فقط Productها را میفرستیم:
uniform vec4 AmbientProduct;
uniform vec4 DiffuseProduct, SpecularProduct;
Ambient ساده است:
ambient = AmbientProduct;
Normal هر رأس به Shader میرسد و Normalize میشود:
in vec4 Normal;
vec3 N = normalize(Normal.xyz);
جهت نور:
vec3 L = normalize(LightPosition - vPosition).xyz;
و Diffuse:
diffuse = max(dot(L, N), 0.0)*DiffuseProduct;
جهت Viewer در مبدأ:
vec3 E = -normalize(vPosition.xyz);
و Halfway Vector:
Specular:
specular = pow(max(dot(N, H), 0.0), Shininess)*SpecularProduct;
و باید در حالتی که نور پشت سطح است سهم Specular را صفر کرد. Shader کامل نمونه:
in vec4 vPosition;
in vec4 Normal;
out vec4 color;
uniform vec4 AmbientProduct, DiffuseProduct, SpecularProduct;
uniform mat4 ModelView;
uniform mat4 Projection;
uniform vec4 LightPosition;
uniform float Shininess;
void main()
{
vec4 ambient, diffuse, specular;
gl_Position = Projection*ModelView*vPosition;
vec3 N = normalize(Normal.xyz);
vec3 L = normalize(LightPosition.xyz - (ModelView*vPosition).xyz);
vec3 E = -normalize((ModelView*vPosition).xyz);
vec3 H = normalize(L+E);
float Kd = max(dot(L, N), 0.0);
float Ks = pow(max(dot(N, H), 0.0), Shininess);
ambient = AmbientProduct;
diffuse = Kd*DiffuseProduct;
specular = max(pow(max(dot(N, H), 0.0), Shininess)
*SpecularProduct, 0.0);
color = vec4((ambient + diffuse + specular).xyz, 1.0);
}
رنگهای رأس سپس توسط Rasterizer Interpolate میشوند.
Fragment Shader ساده:
in vec4 color;
void main()
{
gl_FragColor = color;
}
در مثال مکعب، Uniformهای نور و ماده در برنامه مقداردهی میشوند. مثلاً:
color4 light_ambient = color4(0.2, 0.2, 0.2, 1.0);
color4 material_ambient = color4(1.0, 0.0, 1.0, 1.0);
color4 ambient_product = product(light_ambient, material_ambient);
و به Shader:
GLuint ambient_product_loc;
ambient_product_loc = glGetUniformLocation(program, "AmbientProduct");
glUniform4fv(ambient_product_loc, 4, ambient_product);
Normal چون به بیش از یک رأس وابسته است نمیتواند تنها از Position همان رأس داخل Vertex Shader استخراج شود؛ باید از برنامه یا ساختار دادهٔ Mesh برسد.
در مکعب چرخان بهتر است خود ماتریس دوران/model-view را به Shader بفرستیم، نه اینکه پس از هر تغییر تمام رأسها را دوباره منتقل کنیم. Normal نیز باید همراه شیء دوران کند:
vec4 NN = ModelView*Normal;
vec3 N = normalize(NN.xyz);
برنامهٔ کامل در ضمیمهٔ A است.
برای صحنهٔ چندشیئی یا دوربین متحرک باید دقت بیشتری داشت. مثلاً اگر یک مکعب ثابت و یک مکعب چرخان با دوربین غیرپیشفرض داریم، دو model-view متفاوت لازم است. بخش Viewing مشترک است ولی بخش Modeling یکی ثابت و دیگری متغیر. میتوان model-viewهای کامل را در برنامه ساخت و هر بار فرستاد؛ یا Modeling و Viewing را جدا فرستاد و در Shader الحاق کرد؛ یا حتی فقط زاویهها را به Shader داد. اگر خود نور نیز حرکت کند، گزینهها بیشتر میشوند.
5.9 Shading مدل کره
مکعب چرخان مثال سادهای است، اما به علت فقط شش Face قائم برای سنجش نرمی Lighting مناسب نیست. کرهٔ تقسیمبندیشده بهتر است. هدف آن است که Edgeهای مثلثها دیده نشوند و با Shading نرم، حتی با Polygonهای کمتر ظاهر سطح صاف باقی بماند.
از Shaderهای مکعب استفاده میکنیم، ولی در برنامه به جای Cube Generation از Subdivision چهاروجهی استفاده و Normalها را به صورت Vertex Attribute میفرستیم.
شکل 5.37 ــ مدل کرهٔ Shadeشده. حتی با افزایش Subdivision، ممکن است در پیرامون تصویر Edgeهای Polygon دیده شوند؛ این Edgeهای مرزی Silhouette Edge نام دارند.
تفاوت Constant و Smooth Shading در این مثال درون هر مثلث محدود است، زیرا Normal هندسی مثلث در سه رأس یکسان است؛ با منبع و Viewer دور، Diffuse و Specular نیز تقریباً ثابتاند. اما Triangleهای مجاور Normalهای متفاوت دارند و همین تفاوت، ناهمواری را آشکار میکند.
میتوان برای دیدن حداکثر نرمی با تعداد کم Triangle، از Normal واقعی کره برای هر رأس استفاده کرد. برای کرهٔ واحد در مبدأ، Normal در نقطهٔ (p) همان (p) است؛ بنابراین Position رأس خود Normal است.
void triangle(point4 a, point4 b, point4 c)
{
normals[k] = a;
data[k]= a;
k++;
normals[k] = b;
data[k] = b;
k++;
normals[k] = c;
data[k] = c;
k++;
}
شکل 5.38 ــ Shading کره با Normalهای واقعی. نتیجه بسیار نرمتر از Flat Shading است.
این مثال عمومی نیست، چون Normalها را تحلیلی میدانیم و Gouraud واقعی نیز هنوز پیاده نشده است. برای Gouraud باید در هر رأس Normal همهٔ Polygonهای incident را بدانیم؛ کد فعلی چنین ساختار دادهای ندارد. در Subdivision، معمولاً شش Polygon در رأسهای جدید و سه Polygon در رأسهای اصلی چهاروجهی به هم میرسند.
5.10 نورپردازی Per-Fragment
برای Shading نرمتر میتوان محاسبهٔ Lighting را به جای هر رأس، برای هر Fragment انجام داد. وقتی Lighting کاملاً در Vertex Shader انجام شود، از نظر بصری تفاوتی با محاسبهٔ همان Vertex Colorها روی CPU و فرستادن آنها ندارد؛ در هر دو حالت Rasterizer رنگ رأس را Interpolate میکند.
با Fragment Shader میتوان Lighting مستقل برای هر Fragment داشت. Fragment Shader به Normal، جهت نور و جهت چشمِ Interpolateشده از Rasterizer نیاز دارد. Vertex Shader این بردارها و Position در Clip Coordinates را خروجی میدهد:
in vec4 vPosition;
in vec4 Normal;
uniform mat4 ModelView;
uniform vec4 LightPosition;
uniform mat4 Projection;
out vec3 N;
out vec3 L;
out vec3 E;
void main()
{
gl_Position = Projection*ModelView*vPosition;
N = Normal.xyz;
L = LightPosition.xyz - vPosition.xyz;
if (LightPosition.w == 0.0) L = LightPosition.xyz;
E = vPosition.xyz;
}
Fragment Shader Blinn-Phong:
uniform vec4 AmbientProduct, DiffuseProduct, SpecularProduct;
uniform mat4 ModelView;
uniform vec4 LightPosition;
uniform float Shininess;
in vec3 N;
in vec3 L;
in vec3 E;
void main()
{
vec3 NN = normalize(N);
vec3 EE = normalize(E);
vec3 LL = normalize(L);
vec4 ambient, diffuse, specular;
vec3 H = normalize(LL+EE);
float Kd = max(dot(LL, NN), 0.0);
float Ks = pow(max(dot(NN, H), 0.0), Shininess);
ambient = AmbientProduct;
diffuse = Kd*DiffuseProduct;
if (dot(LL, NN) < 0.0)
specular = vec4(0.0, 0.0, 0.0, 1.0);
else
specular = Ks*SpecularProduct;
gl_FragColor = vec4((ambient + diffuse + specular).xyz, 1.0);
}
بردارها باید در Fragment Shader Normalize شوند؛ Normalize کردن فقط در Vertex Shader تضمین نمیکند Interpolation Rasterizer همچنان بردارهای واحد بدهد.
5.10.1 Shading غیرواقعگرایانه
Shaderهای برنامهپذیر فقط برای واقعگرایی بیشتر نیستند؛ میتوانند سبکهای Nonphotorealistic مانند Cartoon Shading را نیز ایجاد کنند. دو ترفند رایج، محدودکردن تعداد رنگها و تأکید بر Edgeهاست.
مثلاً دو رنگ:
vec4 color1 = vec4(1.0, 1.0, 0.0, 1.0); // yellow
vec4 color2 = vec4(1.0, 0.0, 0.0, 1.0); // red
و انتخاب بر اساس Diffuse:
if (dot(lightv, norm) > 0.5) gl_FrontColor = color1;
else gl_FrontColor = color2;
چون آستانه به Dot Product نور و Normal وابسته است، الگوی رنگ با شکل شیء و جای نور تغییر میکند.
برای Silhouette Edge نیز میتوان نقاطی را که Normal تقریباً عمود بر View Vector است تشخیص داد:
vec4 color3 = vec4(0.0, 0.0, 0.0, 1.0); // black
if (abs(dot(viewv, norm)) < 0.01) gl_FrontColor = color3;
5.11 نورپردازی سراسری
مدل محلی محدودیتهای بنیادی دارد. آرایهای از کرهها زیر منبع دوردست را در نظر بگیرید. کرههای نزدیک منبع بخشی از نور را برای کرههای پشت خود مسدود میکنند، ولی مدل Local هر کره را مستقل Shade میکند و همه تقریباً یکسان دیده میشوند. اگر سطوح Specular باشند، بازتاب کرهها در یکدیگر و حتی بازتابهای چندباره نیز باید دیده شود؛ مدل محلی این اثرها را ندارد. سایههای عمومی نیز با آن به طور مستقیم تولید نمیشوند.
این پدیدهها ــ سایه، بازتاب و انسداد نور ــ Global Effects هستند و به Global Lighting نیاز دارند.
شکل 5.39 ــ آرایهٔ کرههای Shadeشده: (الف) مدل Global، (ب) مدل Local.
Pipeline Renderer هر Polygon را مستقل از دیگران و مستقل از ترتیب تولید برنامه رندر میکند. همین شرط علت سرعت بالا و در عین حال محدودیت در اثرهای Global است.
دو راهبرد مهم جایگزین Ray Tracing و Radiosity هستند و هر یک برای شرایط خاصی مناسبترند. Ray Tracing از مدل دوربین مصنوعی شروع میکند، اما برای هر Projector که به سطح میرسد بررسی میکند آیا آن نقطه واقعاً توسط منابع نور روشن میشود یا نه، سپس Shading محلی را انجام میدهد.
شکل 5.40 ــ Polygon مسدودشده از منبع نور. در مثال، Projector به یک Polygon میرسد، اما نور مستقیم منبع توسط Polygon دیگری مسدود است؛ با این حال ممکن است نور پس از بازتاب از Polygon سوم به نقطه برسد. Ray Tracer میتواند چنین مسیرهایی را پیدا کند.
Radiosity بر بقای انرژی بنا شده است. کل انرژی نور در صحنه باید میان گسیل و بازتاب سطوح توازن داشته باشد؛ بنابراین حل Radiosity مستلزم حل دستگاه بزرگی از معادلات مرتبط با همهٔ سطوح است. Ray Tracing برای صحنههای بسیار بازتابنده مناسبتر و Radiosity برای صحنههای با سطوح عمدتاً Diffuse ایدهآلتر است.
با وجود محدودیت Pipeline Renderer، همچنان میتوان با دانستن اثرهای Global و استفاده از تکنیکهای مناسب آنها را تقریب زد. سایههای Projective فصل ۴ یک نمونهاند. تکنیکهایی مانند Mapping، Multipass Rendering و Transparency در فصلهای بعد برای این هدف استفاده میشوند.
جمعبندی و یادداشتها
مدل نورپردازیای توسعه دادیم که با معماری Pipeline سازگار است و انواع منابع و اثرهای Shading را با سرعت بالا تولید میکند. اگرچه اثرهای کامل Ray Tracer را ندارد، Workstation گرافیکی میتواند صحنهٔ چندضلعی را با Blinn-Phong اصلاحشده و Smooth Shading تقریباً با همان نرخ صحنهٔ بدون Shading رندر کند. از دید برنامهٔ کاربردی، افزودن Shading عمدتاً به تعیین پارامترهای منابع و مواد و پیادهسازی Shaderها نیاز دارد.
Shaderهای برنامهپذیر وضعیت را بهطور اساسی تغییر دادهاند. نهتنها میتوان روشهای جدید Shading رأس ساخت، بلکه Fragment Shader امکان Lighting در هر Fragment را میدهد و نیاز به Interpolate کردن Colorهای نهایی را کاهش میدهد. Phong Shading که در Fixed Pipeline قابل برنامهریزی نبود، اکنون مستقیماً قابل پیادهسازی است و سبکهای Shading بیشماری نیز ممکناند.
Recursive Subdivision مورد استفاده برای کره تکنیکی عمومی است که در فصل ۱۰ برای منحنیها و سطوح و در مدلسازی اشیای طبیعی Self-Similar دوباره ظاهر میشود.
این فصل توسعهٔ گرافیک مبتنی بر Polygon را تکمیل میکند. اکنون باید بتوانید صحنههای دارای نور و Shading بسازید. تکنیکهای پیشرفتهتر مانند Texture Mapping و Compositing از قابلیتهای Pixel-Level بهره میگیرند و در فصل ۷ بررسی میشوند.
زمان مناسبی است که با پارامترهای نور، مواد و منابع متحرک آزمایش کنید. احتمالاً با Artifactهای عددی مانند شکافهای کوچک میان Polygonها مواجه خواهید شد. این خطاها از دقت محدود محاسبات رندر ناشی میشوند و ترفندهای مختلفی برای کاهش آنها وجود دارد.
فصل ۶ وارد جزئیات Rasterization و اجزای خط لوله میشود و نشان میدهد چگونه هر مرحله با مقدار نسبتاً کمی کار اضافه، نتیجهٔ مرحلهٔ قبل را پیش میبرد.
مطالعهٔ پیشنهادی
استفاده از نور و بازتاب در گرافیک رایانهای دو مسیر فیزیکی و محاسباتی داشته است. معادلهٔ رندرینگ Kajiya [Kaj86] توازن انرژی کلی محیط را توصیف و تابع بازتاب هر سطح را لازم میداند. مدلهای بازتاب Torrance-Sparrow [Tor67] و Cook-Torrance [Coo82] سطح را با Facetهای صفحهای ریز مدل میکنند؛ Hall [Hal89] و Foley [Fol90] این مدلها را بررسی کردهاند.
Phong [Pho75] مدل محاسباتی Ambient/Diffuse/Specular را یکپارچه کرد؛ Blinn [Bli77] Halfway Vector را پیشنهاد داد. مدل پایهٔ نور عبوری توسط Whitted [Whi80] استفاده و بعداً توسط Heckbert و Hanrahan [Hec84] اصلاح شد. Gouraud [Gou71] Interpolative Shading را معرفی کرد. OpenGL Programming Guide [Shr10] نیز نکات عملی بسیاری برای رندر و Fixed-Function Lighting قدیمی دارد.
تمرینها
5.1 بیشتر سامانهها مدلهای سادهٔ نور و بازتاب این فصل را برای Polygon Rendering به کار میبرند. خطاهای فیزیکی هر مدل را شرح دهید و صحنهای مثال بزنید که نقص در آن آشکار شود.
5.2 چرا یک Polygon بزرگ که انتظار داریم تقریباً Uniform Shade شود گاهی در بخشی روشن و در بخشی تاریک رندر میشود؟ چگونه میتوان مشکل را کاهش داد؟
5.3 چرا در مدل Phong مسدودشدن منبع نور توسط سطوح دیگر را در مدل بازتاب محلی وارد نکردیم؟
5.4 فاصلهٔ Viewer تا سطح چگونه باید وارد محاسبات رندر شود؟
5.5 برای خواص ماده مدل RGB فرض کردیم. استدلالی برای استفاده از مدل رنگ تفریقی ارائه کنید.
5.6 چهار نقطهٔ همفاصله روی کرهٔ واحد پیدا کنید که یک Tetrahedron میسازند. راهنمایی: یکی را ((0,1,0)) و سه تای دیگر را روی صفحهٔ (y=-d) قرار دهید.
5.7 نشان دهید اگر (v) با (l,n,r) همصفحه باشد Halfway Angle رابطهٔ
2\psi=\phi
را دارد.
اگر (v) همصفحه نباشد رابطهٔ زاویهها چگونه تغییر میکند؟
5.8 همهٔ ترکیبهای Viewer نزدیک/دور، منبع نزدیک/دور، سطح تخت/خمیده و Diffuse/Specular را بررسی کنید. در کدام حالتها محاسبات قابل سادهسازیاند و Halfway Vector کجا سود دارد؟
5.9 ساختار دادهای برای Tetrahedron تقسیمشده بسازید و با پیمایش آن تقریب کره را بر اساس Gouraud Shade کنید.
5.10 تمرین 5.9 را با Icosahedron به جای Tetrahedron انجام دهید.
5.11 ساختار دادهای برای Mesh چهارضلعی بسازید و برنامهای برای Shading آن بنویسید.
5.12 برنامهای برای Recursive Subdivision روی چهارضلعیها و Meshهای چهارضلعی بنویسید.
5.13 دو ماده با مرز صفحهای را فرض کنید که سرعت نور در آنها (v_1,v_2) است. نشان دهید قانون Snell بیان میکند نور مسیر میان دو نقطه در دو ماده را در کمترین زمان طی میکند.
5.14 نشان دهید Halfway Vector (h) زاویهای را مشخص میکند که اگر سطح با آن جهتگیری کند بیشترین نور بازتابی به Viewer میرسد.
5.15 با فرض تابع write_pixel(x, y, color) برای Frame Buffer، شبهکد تابع بازگشتی ray را برای دنبالکردن Ray بنویسید. فرض کنید تابع تقاطع Ray و شیء موجود است. روشی برای محدودکردن عمق دنبالکردن Ray پیشنهاد کنید.
5.16 اگر Pixel-Writing Routine در دسترس است، Ray Tracer سادهای برای صحنهٔ فقط شامل Sphereها بنویسید و از معادلات تحلیلی کره، نه تقریب چندضلعی، استفاده کنید.
5.17 به برنامهٔ Maze تمرین 2.23 منابع نور و Shading اضافه کنید.
5.18 با شروع از برنامهٔ تولید کرهٔ ضمیمهٔ A، برنامهٔ تعاملی برای جایگذاری یک یا چند منبع نور و تغییر خواص ماده بسازید و مواد آشنایی مانند فلزها، پلاستیک و کربن را تقریب بزنید.
5.19 دادهٔ هندسی در خط لوله تحت دوران، انتقال، مقیاسدهی و projection قرار میگیرد. کدام تبدیلها زاویهٔ میان بردارهای مورد استفاده در Cosineهای مدل Phong را حفظ میکنند؟ پیامد آن برای پیادهسازی Shading چیست؟
5.20 مقدار محاسبات اضافی Phong Shading نسبت به Gouraud Shading را تخمین بزنید و نتیجهٔ تمرین 5.19 را در نظر بگیرید.
5.21 اگر جایگاه نور با تبدیل آفین مانند Modeling Transformation تغییر کند، Normal چگونه باید تبدیل شود تا زاویهٔ آن با Light Vector ثابت بماند؟
5.22 Blinn-Phong را دوباره پیادهسازی کنید به گونهای که محاسبات در Eye Coordinates انجام شوند.
5.23 الگوریتم سایهٔ بخش 4.10 را برای سطوح تخت با جهتگیری دلخواه تعمیم دهید.
5.24 الگوریتم سایهٔ بخش 4.10 را برای Distant Source تبدیل کنید. راهنمایی: projection پرسپکتیو باید به Parallel Projection تبدیل شود.
5.25 الگوریتم سایهٔ بخش 4.10 را با روش Global Rendering مقایسه کنید. چه نوع سایههایی با یکی قابل تولید ولی با دیگری غیرممکن است؟
5.26 کرهٔ واحد بسیار بازتابنده در مبدأ را فرض کنید. اگر Viewer در (p) باشد، نقاطی را توصیف کنید که در نقطهای از سطح کره بازتاب و برای Viewer دیده میشوند.