تولید Texture، Environment Mapping، Bump Mapping و ترکیب تصاویر
شکل ۷٫۲۶ — توالی Texture Unitها.
اگر چند Texture Unit مانند شکل ۷٫۲۶ در اختیار داشته باشیم، میتوان چند Texture را بهصورت مرحلهای روی یک Fragment اعمال کرد. هر Unit یک مرحلهٔ مستقل Texturing است که ورودی آن نتیجهٔ مرحلهٔ قبلی است. این قابلیت در نسخههای جدید OpenGL پشتیبانی میشود.
فرض کنید بخواهیم از دو Texture Unit استفاده کنیم. در مرحلهٔ مقداردهی اولیه دو Texture Object تعریف میکنیم، سپس هر کدام را بهترتیب فعال کرده و شیوهٔ اعمال Texture آن را تعیین میکنیم. الگوی رایج کد چنین است:
glActiveTexture(GL_TEXTURE0); /* unit 0 */
glBindTexture(GL_TEXTURE_2D, object0);
/* how to apply texture 0 */
glActiveTexture(GL_TEXTURE1); /* unit 1 */
glBindTexture(GL_TEXTURE_2D, object1);
/* how to apply texture 1 */
هر Texture Unit میتواند Texture Coordinateهای متفاوتی داشته باشد و برنامهٔ کاربردی باید Texture Coordinateهای مربوط به هر Unit را جداگانه فراهم کند.
۷٫۷ تولید Texture
یکی از قدرتمندترین کاربردهای Texture Mapping افزودن جزئیات بدون تولید تعداد زیادی شیء هندسی است. سامانههای گرافیکی قدرتمند میتوانند Texture Mapping دوبعدی را در زمان واقعی انجام دهند؛ در هر Frame، Texture بخشی از فرایند Rendering روی اشیا نگاشت میشود و سرعت پردازش تقریباً نزدیک به اشیای بدون Texture است. کارتهای گرافیکی رایانههای شخصی نیز مقدار قابلتوجهی Texture Memory دارند و به توسعهدهندگان بازی اجازه میدهند محیطهای متحرک پیچیده بسازند.
برای مثال، اگر بخواهیم چمن را در یک صحنه شبیهسازی کنیم، نگاشت تصویر چمن—مثلاً تصویری که از یک عکس اسکن شده—بسیار سریعتر از تولید اشیای دوبعدی یا سهبعدی متعدد شبیه به تیغههای چمن است. در برنامههای نقشهبرداری نیز بهجای ساخت جزئیات هندسی واقعگرایانهٔ سطح زمین میتوان یک نقشهٔ واقعی را دیجیتال کرد و با Texture Mapping آن را روی مدل سطح سهبعدی نقاشی نمود.
همچنین میتوان روشهای رویهای برای تعیین الگوهای Texture به کار برد. الگوهای طبیعی مانند شن، چمن یا کانیها اهمیت ویژهای دارند، زیرا هم دارای ساختار و الگوهای منظم هستند و هم میزان زیادی تصادفیبودن دارند. بسیاری از روشهای تولید الگوریتمی این نوع Texture با یک مولد عدد تصادفی آغاز میشوند و خروجی آن را پردازش میکنند؛ شکل ۷٫۲۷ این فرایند را نشان میدهد. Procedural Noise در فصل ۹ با جزئیات بررسی خواهد شد.
شکل ۷٫۲۷ — تولید Texture: Noise → فرایند الگوریتمی → Texture.
تولید میدان Texture سهبعدی (T(s,t,r)) تعمیم مستقیمی از روشهای تولید Texture دوبعدی است. Textureهای سهبعدی چند مزیت عملی دارند. مهمترین مزیت این است که با مرتبطکردن مستقیم هر مقدار (s,t,r) با یک نقطهٔ (x,y,z)، مسئلهٔ Mapping تقریباً حذف میشود. کاربر فقط باید تابعی (T(s,t,r)) با ویژگیهای مطلوب تعریف کند.
از نظر مفهومی، این فرایند شبیه تراشیدن یک شیء سهبعدی از یک بلوک جامد است که حجم آن با Texture مشخص رنگ شده است. از این روش برای تولید اشیایی استفاده شده که گویی از سنگ یکپارچه تراشیده شدهاند. فرایند تولید Texture تابعی (T(s,t,r)) تعریف میکند که دانهبندی مواد طبیعی مانند مرمر و گرانیت را بازنمایی میکند.
Texture سهبعدی مزیت دیگری نیز دارد. فرض کنید Texture دوبعدیای از یک مادهٔ طبیعی مانند سنگ، با عکاسی یا مدلسازی، در اختیار داریم و میخواهیم مکعبی بسازیم که از همان سنگ ساخته شده به نظر برسد. با Texture Mapping دوبعدی باید همان الگو را روی شش وجه مکعب نگاشت کنیم و برای طبیعیبودن ظاهر، Textureها را در لبههای مکعب که دو نقشه به هم میرسند پیوسته کنیم. در الگوهای طبیعی تضمین چنین تطبیقی تقریباً غیرممکن است. مسئله در رأسهای مکعب، جایی که سه Texture Map به هم میرسند، حتی دشوارتر است. Filtering و Texture Border گاهی نتیجهٔ بصری قابل قبول ایجاد میکنند، اما در Texture سهبعدی اساساً چنین مسئلهای وجود ندارد.
۷٫۸ Environment Mapها
سطوح بسیار بازتابنده دارای بازتابهای Specular هستند که محیط اطراف را مانند آینه منعکس میکنند. برای مثال یک کرهٔ فلزی براق در وسط اتاق میتواند محتوای اتاق را بهشکل تحریفشده روی سطح خود نشان دهد. این اثر به اطلاعات سراسری نیاز دارد، زیرا بدون دانستن سایر اجزای صحنه نمیتوان رنگ کره را درست محاسبه کرد.
روشهای Rendering فیزیکی مانند Ray Tracing قادرند چنین تصویری تولید کنند، اما محاسبات Ray Tracing معمولاً برای کاربردهای Real-Time پرهزینهاند. در عوض میتوان از گونههایی از Texture Mapping، مانند Environment Map یا Reflection Map، برای بهدستآوردن تقریبهای بصری مناسب استفاده کرد.
ایدهٔ اصلی ساده است. آینهٔ شکل ۷٫۲۸ را بهصورت یک Polygon با مادهای بسیار Specular در نظر بگیرید. Renderer موقعیت بیننده و Normal چندضلعی را میداند، بنابراین زاویهٔ بازتاب طبق روابط فصل ۵ تعیین میشود. اگر در امتداد این جهت حرکت کنیم تا با محیط برخورد شود، رنگ بازتابیدهشده در آینه به دست میآید.
شکل ۷٫۲۸ — صحنهای شامل آینه.
رنگ بازتابی نتیجهٔ Shading بر مبنای منابع نور و مواد صحنه است. میتوان تقریب خوبی از آن را در یک Rendering دوگذری به دست آورد. در Pass نخست صحنه را بدون Polygon آینه Render میکنیم، دوربین را در مرکز آینه قرار میدهیم و آن را در جهت Normal آینه نشانه میرویم. در نتیجه تصویری از محیط، آنگونه که آینه آن را «میبیند»، به دست میآید. این تصویر کاملاً دقیق نیست، اما معمولاً کافی است. سپس در Pass دوم، تصویر بهعنوان مقادیر Texture/رنگ روی آینه استفاده میشود و صحنه با خود آینه بهطور عادی Render میشود.
دو دشواری وجود دارد. نخست، تصویر Pass اول کاملاً درست نیست، چون یکی از اشیای محیط—خود آینه—در آن حضور ندارد. دوم، مسئلهٔ Mapping مطرح است: صحنه در Pass اول روی چه سطحی Project شود و دوربین کجا قرار گیرد؟ ممکن است به اطلاعات همهٔ جهتهای محیط نیاز داشته باشیم؛ مثلاً اگر آینه حرکت کند، در Frameهای متوالی بخشهای متفاوتی از محیط باید دیده شوند. در این حالت یک Projection ساده کافی نیست.
برای حل این مسئله روشهای گوناگونی پیشنهاد شدهاند. روش کلاسیک، Projectکردن محیط روی کرهای است که مرکز آن در Center of Projection قرار دارد. در شکل ۷٫۲۹ تعدادی Polygon بیرون کره و Projection آنها روی سطح کره دیده میشود. بینندهای در مرکز کره نمیتواند تشخیص دهد Polygonهای واقعی را میبیند یا تصویر آنها روی کره را. این توهم مشابه چیزی است که در Planetarium رخ میدهد: «ستارههایی» که بسیار دور به نظر میرسند در حقیقت Projection نورها روی نیمکرهٔ پیرامون تماشاگران هستند.
در نخستین روشهای Environment Mapping، سطح کره با استفاده از خطوط طول و عرض جغرافیایی به مستطیل تبدیل میشد. با وجود سادگی مفهومی، نزدیک قطبها اعوجاج شکل نامتناهی میشود. این نگاشت از نظر حفظ مساحت نیز مناسب نیست و به محاسبهٔ تعداد زیادی تابع مثلثاتی نیاز دارد.
شکل ۷٫۲۹ — نگاشت محیط روی یک سطح میانی.
OpenGL از گونهای از این روش با نام Sphere Mapping پشتیبانی میکند. برنامهٔ کاربردی یک تصویر دایرهای فراهم میکند که Orthographic Projection کرهای است که محیط روی آن نگاشت شده است. مزیت این روش آن است که نگاشت Reflection Vector به Texture Coordinateهای دوبعدی روی این دایره ساده است و میتواند در سختافزار یا نرمافزار پیاده شود.
بخش دشوار، تهیهٔ تصویر دایرهای لازم است. این تصویر را میتوان با Perspective Projection دارای لنز بسیار Wide-Angle تقریب زد، یا Projection دیگری مانند Cube Projection را که در ادامه میآید دوباره نگاشت کرد. Texture Image با glTexImage2D در Texture Memory قرار میگیرد.
معادلات تولید Texture Coordinate را میتوان با شکل ۷٫۳۰ درک کرد. آسانتر است از بیننده به سمت تصویر به عقب حرکت کنیم. فرض کنید Texture Map در صفحهٔ (z=-d) قرار دارد که (d) مثبت است و از آن بهطور Orthogonal به سمت یک Unit Sphere با مرکز مبدأ Project میکنیم.
شکل ۷٫۳۰ — Reflection Map.
اگر مختصات Texture در صفحه (s,t) باشند، خط Projector کره را در نقطهٔ
(s,t,\sqrt{1-s^2-t^2})
قطع میکند. برای Unit Sphere مرکز مبدأ، مختصات هر نقطه روی کره همان مؤلفههای Unit Normal آن نقطه نیز هستند. در نتیجه جهت بازتاب، مطابق فصل ۵، چنین محاسبه میشود:
r=2(n\cdot v)n-v,
که در آن
v=\begin{bmatrix}s\\t\\0\end{bmatrix},\qquad
n=\begin{bmatrix}s\\t\\\sqrt{1-s^2-t^2}\end{bmatrix}.
بردار r به درون محیط اشاره میکند؛ بنابراین هر شیئی که r به آن برخورد کند Texture Coordinateهای (s,t) را خواهد داشت. البته این استدلال معکوس است، زیرا در عمل از شیئی تعریفشده با رأسها آغاز میکنیم. اگر
r=\begin{bmatrix}r_x\\r_y\\r_z\end{bmatrix},
با حل روابط برای s و t به دست میآید:
s=\frac{r_x}{f}+\frac12,\qquad
t=\frac{r_y}{f}+\frac12,
که در آن
f=2\sqrt{r_x^2+r_y^2+(r_z+1)^2}.
اگر همهچیز را در Eye Coordinates بیان کنیم، برای v از Unit Vector مبدأ به رأس و برای n از Vertex Normal استفاده میشود.
این فرایند نشان میدهد روش فوق فقط یک تقریب است. Reflection Map تنها برای رأسی که در مبدأ قرار دارد دقیق است. در اصل هر رأس باید Reflection Map مخصوص خود را داشته باشد؛ حتی دقیقتر آن است که هر نقطهٔ شیء Map مستقل داشته باشد، نه مقداری تقریبی که از درونیابی رأسها به دست میآید. خطا هرچه شیء از مبدأ دورتر باشد بیشتر میشود. بااینحال Reflection Mapping در بسیاری از کاربردها نتیجهای از نظر بصری قابل قبول میدهد.
شکل ۷٫۳۱ — Cube Map بازتابی.
Reflection Mapping در اغلب وضعیتها نتیجهٔ بصری قابل قبولی میدهد، بهویژه در تصاویر متحرک مانند فیلمها و بازیها که خطاهای کوچک کمتر به چشم میآیند.
اگر بخواهیم Environment Map را با خود سامانهٔ گرافیکی محاسبه کنیم، بهتر است از Projectionهای استاندارد پشتیبانیشده استفاده کنیم. برای محیطی مانند یک اتاق، شیء میانی طبیعی یک Box است. شش Projection متناظر با دیوارها، کف و سقف محاسبه میکنیم؛ شش دوربین مجازی در مرکز Box قرار میگیرند و هر کدام در جهتی متفاوت نگاه میکنند. سپس شش تصویر را میتوان یک Environment Map واحد در نظر گرفت و Textureهای بازتاب را از آن استخراج کرد؛ همانطور که در شکل ۷٫۳۱ نشان داده شده است. در نمونهٔ مشهور فیلم Geri’s Game از Pixar، Reflection Map روی یک Box محاسبه و سپس روی عینک شخصیت Geri نگاشت شد.
همچنین میتوان شش تصویر را در برنامه محاسبه و از آنها تصویر دایرهای مورد نیاز Sphere Map در OpenGL را ساخت. همهٔ این روشها ممکن است دچار اعوجاج هندسی و مشکلات Aliasing شوند. افزون بر آن، اگر Viewer حرکت کند، Environment Map دیگر دقیق نیست مگر آنکه برنامه آن را دوباره محاسبه کند.
صرفنظر از روش محاسبهٔ تصاویر، پس از آمادهشدن آنها میتوان Cube Map را با شش فراخوانی در OpenGL تعریف کرد؛ یک تصویر برای هر وجه مکعبی با مرکز مبدأ. برای مثال اگر تصویر RGBA با اندازهٔ (512\times512) و نام imagexp مربوط به وجه مثبت محور x باشد:
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, 512, 512,
0, GL_RGBA, GL_UNSIGNED_BYTE, imagexp);
در Reflection Map میتوان Texture Coordinateها را خودکار محاسبه کرد. بااینحال Cube Map از نظر بنیادی با Sphere Map متفاوت است؛ Sphere Map بسیار شبیه Texture Map دوبعدی استاندارد با محاسبات مختصات ویژه است، در حالی که Cube Map به Texture Coordinateهای سهبعدی نیاز دارد که غالباً در Shader محاسبه میشوند.
این روشها نمونههایی از Multipass Rendering یا Multirendering هستند: برای محاسبهٔ یک تصویر نهایی، چند تصویر میانی تولید میشوند که هرکدام از Rendering Pipeline عبور میکنند. با افزایش قدرت کارتهای گرافیکی، این روشها اهمیت بیشتری یافتهاند، زیرا میتوان یک صحنه را چند بار از دیدگاههای مختلف Render کرد و همچنان به Refresh Rate مناسب رسید. بسیاری از این تکنیکها را نیز میتوان مستقیماً در Fragment Shader پیادهسازی کرد.
۷٫۹ مثال Reflection Map
یک Reflection Map ساده را بر پایهٔ مثال مکعب چرخان بررسی میکنیم. در این مثال Cube Map شامل شش Texture Map است که هر کدام فقط یک Texel دارند. مکعب چرخان کاملاً بازتابنده است و داخل Boxی قرار دارد که شش وجه آن بهترتیب یکی از رنگهای قرمز، سبز، آبی، فیروزهای، ارغوانی و زرد هستند.
Cube Map را در زمان مقداردهی اولیه و با Texture Unit شمارهٔ ۱ چنین میسازیم:
GLuint tex[1];
GLubyte red[3] = {255, 0, 0};
GLubyte green[3] = {0, 255, 0};
GLubyte blue[3] = {0, 0, 255};
GLubyte cyan[3] = {0, 255, 255};
GLubyte magenta[3] = {255, 0, 255};
GLubyte yellow[3] = {255, 255, 0};
glActiveTexture(GL_TEXTURE1);
glGenTextures(1, tex);
glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X ,0,GL_RGB,1,1,0,GL_RGB,
GL_UNSIGNED_BYTE, red);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X ,0,GL_RGB,1,1,0,GL_RGB,
GL_UNSIGNED_BYTE, green);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y ,0,GL_RGB,1,1,0,GL_RGB,
GL_UNSIGNED_BYTE, blue);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y ,0,GL_RGB,1,1,0,GL_RGB,
GL_UNSIGNED_BYTE, cyan);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z ,0,GL_RGB,1,1,0,GL_RGB,
GL_UNSIGNED_BYTE, magenta);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z ,0,GL_RGB,1,1,0,GL_RGB,
GL_UNSIGNED_BYTE, yellow);
glTexParameteri(GL_TEXTURE_CUBE_MAP,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
شکل ۷٫۳۲ — Reflection Cube Map.
Texture Map از طریق Sampler در Fragment Shader اعمال میشود. Uniform Variable لازم را مانند مثالهای پیشین تنظیم میکنیم:
GLuint texMapLocation;
texMapLocation = glGetUniformLocation(program, "texMap");
glUniform1i(texMapLocation, 1); // corresponding to unit 1
اکنون که Cube Map آماده است، باید Texture Coordinateها را تعیین کنیم. محاسبات لازم برای Reflection/Environment Map در شکل ۷٫۳۲ نشان داده شدهاند. فرض میکنیم محیط از قبل روی Cube نگاشت شده است. تفاوت Reflection Map با Cube Texture Map ساده این است که برای دسترسی به Texture از Reflection Vector استفاده میکنیم، نه View Vector.
میتوان Reflection Vector را در Vertex Shader برای هر رأس محاسبه کرد و سپس Fragment Shader مقادیر درونیابیشده روی Primitive را دریافت کند. برای محاسبهٔ Reflection Vector به Normal هر وجه مکعب چرخان نیاز داریم. Normalها را میتوان در برنامه محاسبه و از طریق تابع quad بهعنوان Vertex Attribute به Vertex Shader ارسال کرد:
point4 normals[N];
vec4 normal;
void quad(int a, int b, int c, int d)
{
static int i =0;
normal = normalize(cross(vertices[b]-vertices[a],
vertices[c]-vertices[b]));
normals[i] = normal;
points[i] = vertices[a];
i++;
normals[i] = normal;
points[i] = vertices[b];
i++;
normals[i] = normal;
points[i] = vertices[c];
i++;
normals[i] = normal;
points[i] = vertices[a];
i++;
normals[i] = normal;
points[i] = vertices[c];
i++;
normals[i] = normal;
points[i] = vertices[d];
i++;
}
دادهٔ Normal و Position را در یک Vertex Array ترکیب میکنیم:
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(points) + sizeof(normals),
NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(points), points);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(points),
sizeof(normals), normals)
و ورودی Shader را با آن همتراز میکنیم:
loc2 = glGetAttribLocation(program, "Normal");
glEnableVertexAttribArray(loc2);
فرض میکنیم Rotation مکعب در برنامه اعمال شده و اثر آن در Model-View Matrix قرار گرفته است؛ همچنین محل دوربین ثابت است. بنابراین Normalها باید در Vertex Shader نیز Rotate شوند تا سپس تابع reflect بتواند جهت بازتاب را محاسبه کند. Vertex Shader چنین است:
in vec4 vPosition;
in vec4 Normal;
out vec3 R;
uniform mat4 ModelView;
uniform mat4 Projection;
void main()
{
gl_Position = Projection*ModelView*vPosition;
vec3 eyePos = vPosition.xyz;
vec4 NN = ModelView*Normal;
vec3 N = normalize(NN.xyz);
R = reflect(eyePos, N);
}
این Shader بردار بازتاب را در Eye Coordinates محاسبه و آن را بهصورت یک Varying Variable خروجی میدهد. اگر بخواهیم رنگ کاملاً توسط Texture تعیین شود، Fragment Shader بسیار ساده خواهد بود:
in vec3 R;
uniform samplerCube texMap;
void main()
{
vec4 texColor = textureCube(texMap, R);
gl_FragColor = texColor;
}
برای نورپردازی پیچیدهتر میتوان رنگ نهایی را ترکیبی از مؤلفههای Specular، Diffuse و Ambient—مانند مدل Modified Phong—و رنگ Texture در نظر گرفت.
بااینحال باید دقت کنیم Shaderها در کدام Frame مختصاتی کار میکنند. تفاوت این مثال با مثالهای قبلی آن است که Environment Map معمولاً در World Coordinates محاسبه میشود. Position و Normal اشیا ابتدا در Object Coordinates تعریف میشوند و با Modeling Transformation در برنامه به World Frame منتقل میگردند. معمولاً نمایش Object Coordinates را مستقیماً نمیبینیم، زیرا Model-View Transformation آنها را مستقیماً به Eye Coordinates تبدیل میکند.
در بسیاری از برنامهها اشیا بدون Modeling Transformation تعریف میشوند و در نتیجه Model Coordinates و Object Coordinates یکساناند. اما برای Reflection Mapping باید برنامه بهگونهای نوشته شود که Modeling Transformation را نیز پشتیبانی کند. یک راه این است که Modeling Matrix در برنامه محاسبه و بهصورت Uniform Variable به Fragment Shader ارسال شود. برای تبدیل Normal نیز به Inverse Transpose ماتریس مدلسازی نیاز داریم؛ اگر Inverse Matrix را نیز بهصورت Uniform ارسال کنیم، میتوان Normal را بهطور مناسب تبدیل کرد.
نمونهٔ تصویری کتاب استفاده از Reflection Map برای تعیین رنگهای قوری را نشان میدهد: قوری داخل مکعبی قرار دارد که وجوه آن به شش رنگ قرمز، سبز، آبی، فیروزهای، ارغوانی و زرد هستند.
۷٫۱۰ Bump Mapping
Bump Mapping تکنیکی در Texture Mapping است که بدون افزایش پیچیدگی هندسی، ظاهر سطح را بسیار پیچیدهتر نشان میدهد. برخلاف Texture Mapping ساده، با حرکت منبع نور یا خود شیء، Shading در Bump Mapping تغییر میکند و سطح دارای ناهمواریهای واقعی به نظر میرسد.
به مثال ساخت تصویر یک پرتقال برگردیم. اگر از یک پرتقال واقعی عکس بگیریم، میتوانیم تصویر را بهصورت Texture Map روی یک سطح اعمال کنیم. اما بهمحض جابهجایی نورها یا چرخاندن شیء، مشخص میشود با تصویرِ یک مدل پرتقال روبهرو هستیم، نه با خود پرتقال واقعی. علت این است که ویژگی اصلی پوست پرتقال بیشتر از تغییرات کوچک هندسهٔ سطح ناشی میشود تا تغییرات رنگ؛ Texture Mapping عادی این ناهمواریهای ریز را ثبت نمیکند.
Bump Mapping با Perturbکردن Normal Vectorها هنگام Rendering، شکل ظاهری سطح را تغییر میدهد. رنگهایی که از Shading حاصل میشوند سپس تغییرات ویژگی سطح را نشان میدهند. برخلاف روشهایی مانند Environment Mapping که حتی بدون Programmable Shader نیز قابل پیادهسازیاند، Bump Mapping برای اجرای Real-Time عملاً به Shaderهای قابل برنامهریزی وابسته است.
۷٫۱۰٫۱ یافتن Bump Map
Normal هر نقطه روی سطح جهتگیری موضعی سطح را مشخص میکند. اگر Normal هر نقطه را اندکی تغییر دهیم، سطحی با تغییرات کوچک شکل ایجاد میشود. اگر این تغییر فقط هنگام Shading اعمال شود، میتوان مدل هندسی خود سطح را صاف و ساده نگه داشت ولی آن را طوری Shade کرد که پیچیده و ناهموار به نظر برسد. چون Perturbation روی Normalها اعمال میشود، محاسبات Rendering برای سطحِ ظاهراً تغییرکرده درست باقی میمانند، بیآنکه لازم باشد هندسهٔ پیچیدهٔ متناظر واقعاً ساخته شود.
برای Perturbکردن Normal روشهای مختلفی وجود دارد. روش زیر برای سطوح پارامتری کارآمد است. فرض کنید (p(u,v)) نقطهای روی سطح پارامتری باشد. مشتقهای جزئی
p_u=\begin{bmatrix}\partial x/\partial u\\\partial y/\partial u\\\partial z/\partial u\end{bmatrix},\qquad
p_v=\begin{bmatrix}\partial x/\partial v\\\partial y/\partial v\\\partial z/\partial v\end{bmatrix}
در صفحهٔ مماس بر سطح در آن نقطه قرار میگیرند. Cross Product آنها پس از Normalizeشدن Unit Normal را میدهد:
n=\frac{p_u\times p_v}{|p_u\times p_v|}.
اکنون فرض کنید سطح را در جهت Normal به اندازهٔ تابع کوچکی با نام Bump/Displacement Function یعنی (d(u,v)) جابهجا کنیم و (|d(u,v)|\ll1) باشد. سطح جابهجاشده:
p'=p+d(u,v)n.
نمیخواهیم خود این سطح پیچیدهتر را بسازیم، چون پیچیدگی هندسی و هزینهٔ Rendering افزایش مییابد. هدف این است که فقط ظاهر جابهجایی را با تغییر Normal ایجاد کنیم.
Normal در نقطهٔ تغییرکرده (p') از Cross Product مشتقهای جزئی آن به دست میآید:
n'=p'_u\times p'_v.
با مشتقگیری از رابطهٔ (p'):
p'_u=p_u+\frac{\partial d}{\partial u}n+d(u,v)n_u,
p'_v=p_v+\frac{\partial d}{\partial v}n+d(u,v)n_v.
اگر (d) کوچک باشد، جملههای متناسب با خود (d) را میتوان نادیده گرفت. با Cross Product دو رابطه و با توجه به (n\times n=0)، تقریب Normal تغییرکرده چنین میشود:
n'\approx n+\frac{\partial d}{\partial u}(n\times p_v)+\frac{\partial d}{\partial v}(n\times p_u).
دو جملهٔ سمت راست، Displacement میان Normal اولیه و Normal Perturbed را میسازند. Cross Product دو بردار بر هر دو عمود است؛ بنابراین هر دو بردار حاصل در Tangent Plane نقطهٔ (p) قرار دارند و جمع آنها نیز در همان صفحه است.
بردارهای (p'_u) و (p'_v) هر دو در Tangent Plane عمود بر (n') هستند، اما لزوماً بر یکدیگر عمود نیستند. برای ساخت یک Basis متعامد، ابتدا (n') و (p'_u) را Normalize میکنیم:
m=\frac{n'}{|n'|},\qquad t=\frac{p'_u}{|p'_u|}.
بردار متعامد سوم:
b=m\times t.
t Tangent Vector و b Binormal Vector در نقطهٔ (p) نام دارند. ماتریس
M=[\,t\quad b\quad m\,]^T
Rotation Matrixای است که نمایش بردارها را از فضای اولیه به Basis محلی این سه بردار تبدیل میکند. این فضای جدید Tangent Space نام دارد.
چون Tangent و Binormal ممکن است در هر نقطه از سطح تغییر کنند، Tangent Space یک Coordinate System محلی است.
برای روشنشدن مفهوم Frame محلی، Bump روی صفحهٔ (z=0) را در نظر بگیرید. سطح را میتوان بهصورت ضمنی چنین نوشت:
f(x,y)=ax+by+c=0.
اگر (u=x) و (v=y) بگیریم و (a\neq0) باشد، نمایش پارامتری متناظر را میتوان نوشت و مشتقهای (\partial p/\partial u) و (\partial p/\partial v) پس از Normalizeشدن در حالت ساده به بردارهای متعامد
[1\ 0\ 0]^T,\qquad [0\ 1\ 0]^T
میرسند؛ در نتیجه همینها Tangent و Binormal هستند و Unit Normal برابر است با
n=[0\ 0\ 1]^T.
در این حالت Displacement Function تابعی از (x,y)، یعنی (d(x,y))، است. برای مشخصکردن Bump Map به دو تابع (\partial d/\partial x) و (\partial d/\partial y) نیاز داریم. اگر این مشتقها بهصورت تحلیلی معلوم باشند میتوان آنها را در برنامه یا Shader محاسبه کرد. اما معمولاً نسخهٔ Sampleشدهٔ (d(x,y)) را بهصورت آرایهای از Pixelها، (D=[d_{ij}])، داریم. مشتقها را میتوان با تفاضل عناصر مجاور تقریب زد:
\frac{\partial d}{\partial x}\propto d_{ij}-d_{i-1,j},
\frac{\partial d}{\partial y}\propto d_{ij}-d_{i,j-1}.
این آرایهها را میتوان از قبل در برنامه محاسبه و بهصورت Textureای با نام Normal Map ذخیره کرد. Fragment Shader از طریق Sampler به این مقادیر دسترسی خواهد داشت.
برای سطح عمومی که صفحهٔ (z=0) نیست، Tangent Space دیگر با محورهای Object یا World همراستا نیست. Normal سطح لزوماً در جهت z یا هیچ محور خاصی قرار ندارد، و Tangent/Binormal نیز جهت ثابتی نسبت به محورهای World یا Object ندارند.
Displacement در امتداد Normal اندازهگیری میشود، بنابراین مشتقهای آن در صفحهای با جهت دلخواه قرار میگیرند. اما در Tangent Space این جابهجایی در امتداد محور z محلی است. اهمیت ماتریس (M)، متشکل از Normal، Tangent و Binormal، در این است که ما را به Coordinate System محلیای میبرد که محاسبات Bump Map دقیقاً با حالت سادهٔ قبلی منطبق است.
پیادهسازی رایج Bump Mapping این ماتریس را پیدا میکند و بردارهای Object Space را به Tangent Space محلی تبدیل میکند. چون Tangent Space محلی است، این تبدیل نمایش میتواند برای هر Fragment متفاوت باشد. در Polygon Meshها اگر روی هر Polygon از Normal ثابت استفاده شود، محاسبه سادهتر است و برنامه میتواند Tangent و Binormal را یک بار برای هر Polygon به Vertex Shader بفرستد.
اکنون تقریباً آمادهٔ نوشتن Vertex و Fragment Shader هستیم. کمیتهای لازم برای Lighting—Surface Normal، Light Vectorها، Half-Angle Vector و Vertex Location—معمولاً در زمانی که نورپردازی انجام میشود در Eye یا Object Coordinates قرار دارند. اما چه Normal Map استفاده کنیم و چه Perturbation را Procedural در Fragment Shader محاسبه کنیم، Displacementها در Texture-Space Coordinates هستند.
برای Shading درست باید یا Normal Map را به Object Space تبدیل کنیم یا بردارهای Object Space را به Texture/Tangent Space ببریم. گزینهٔ دوم معمولاً کمهزینهتر است، چون میتواند بهصورت Per-Vertex در Vertex Shader انجام شود، نه Per-Fragment. ماتریس این تبدیل همان ماتریس ساختهشده از Normal، Tangent و Binormal است.
اگر Normal در هر رأس تغییر میکند، آن را بهعنوان Vertex Attribute میفرستیم؛ اگر در هر بار یک Polygon تخت پردازش شود، Uniform Variable کافی است. Tangent Vectorها نیز به همین شکل از برنامه ارسال میشوند و Binormal را میتوان در Shader با Cross Product محاسبه کرد. Vertex Shader سپس Light Vector و View Vector را در Tangent Coordinates تولید میکند. چون Normal در Tangent Space همواره در جهت مثبت z است، همین دو بردار برای انجام Lighting در Tangent Space کافیاند.
۷٫۱۰٫۲ مثال Bump Map
مثال ما یک مربع در صفحهٔ (y=0) است و منبع نوری بالای صفحه، در صفحهٔ (y=10.0)، میچرخد. برای کوتاهماندن کد فقط Diffuse Lighting را در نظر میگیریم. Displacement یک مربع کوچک در مرکز مربع اصلی است. شکل ۷٫۳۳ دو نتیجه را نشان میدهد: یک بار منبع نور در وضعیت اولیه و یک بار پس از چرخش ۴۵ درجه در صفحهٔ x-z، با همان ارتفاع از سطح.
مربع با دو Triangle ساخته میشود و هر شش رأس دارای Texture Coordinate است؛ بنابراین این قسمت مشابه مثالهای قبلی است.
شکل ۷٫۳۳ — Bump Mapping یک جابهجایی مربعی.
point4 points[6];
point2 tex_coord[6];
void mesh()
{
point4 vertices[4] = {
point4(0.0, 0.0, 0.0, 1.0),
point4(1.0, 0.0, 0.0, 1.0),
point4(1.0, 0.0, 1.0, 1.0),
point4(0.0, 0.0, 1.0, 1.0)
};
points[0] = vertices[0];
tex_coord[0] = point2(0.0, 0.0);
points[1] = vertices[1];
tex_coord[1] = point2(1.0, 0.0);
points[2] = vertices[2];
tex_coord[2] = point2(1.0, 1.0);
points[3] = vertices[2];
tex_coord[3] = point2(1.0, 1.0);
points[4] = vertices[3];
tex_coord[4] = point2(0.0, 1.0);
points[5] = vertices[0];
tex_coord[5] = point2(0.0, 0.0);
}
این دادهها بهصورت Vertex Attribute به GPU ارسال میشوند.
Displacement Map بهشکل یک آرایه در برنامه تولید میشود. دادهٔ Displacement در آرایهٔ data قرار میگیرد. Normal Map با گرفتن تفاضل عناصر مجاور برای تقریب دو مشتق جزئی و استفاده از مقدار 1.0 برای مؤلفهٔ سوم ساخته میشود و در آرایهٔ normals قرار میگیرد. چون این مقادیر بهصورت Color در یک Texture Image ذخیره میشوند، مؤلفهها به بازهٔ (0.0, 1.0) Scale میشوند.
const int N = 256;
float data[N+1][N+1];
vec3 normals[N][N];
for(int i = 0; i < N+1; i++)
for(int j = 0; j < N+1; j++)
data[i][j]=0.0;
for(int i = N/4; i < 3*N/4; i++)
for(int j = N/4; j < 3*N/4; j++)
data[i][j] = 1.0;
for(int i = 0;i < N; i++)
for(int j = 0;j < N; j++)
{
vec4 n = vec3(data[i][j] - data[i+1][j], 0.0, data[i][j] -
data[i][j+1]);
normals[i][j] = 0.5*normalize(n) + 0.5;
}
سپس آرایهٔ normals با ساخت یک Texture Object به GPU فرستاده میشود. Projection Matrix، Model-View Matrix، موقعیت منبع نور و پارامترهای Diffuse Lighting نیز بهصورت Uniform Variable به Shaderها ارسال میشوند. چون سطح تخت است، Normal ثابت است و میتواند Uniform باشد. Tangent Vector نیز ثابت است و میتواند هر برداری در همان صفحهٔ Polygon باشد؛ این بردار هم بهصورت Uniform به Vertex Shader میرود.
در Vertex Shader میخواهیم محاسبات در Texture Space انجام شوند، بنابراین Light Vector و Eye/View Vector باید به این فضا تبدیل شوند. Transformation Matrix لازم از Normal، Tangent و Binormal ساخته میشود.
Normal و Tangent در Object Coordinates مشخص شدهاند و ابتدا باید به Eye Coordinates تبدیل شوند. ماتریس لازم Normal Matrix است؛ یعنی Inverse Transpose زیرماتریس (3\times3) بالا-چپ Model-View Matrix. فرض میکنیم این ماتریس در برنامه محاسبه و بهصورت Uniform دیگری به Shader ارسال شده است. سپس از Normal و Tangent تبدیلشده برای محاسبهٔ Binormal در Eye Coordinates استفاده میکنیم و این سه بردار Light Vector و View Vector را به Texture Space میبرند.
Vertex Shader:
/* bump map vertex shader */
out vec3 L; /* light vector in texture-space coordinates */
out vec3 V; /* view vector in texture-space coordinates */
out vec2 st; /* texture coordinates */
in vec2 texcoord;
in vec4 vPosition;
uniform vec3 Normal;
uniform vec4 LightPosition;
uniform mat4 ModelView;
uniform mat4 Projection;
uniform mat4 NormalMatrix;
uniform vec3 objTangent;
void main()
{
gl_Position = Projection*ModelView*vPosition;
st = texcoord;
vec3 eyePosition = vec3(ModelView*vPosition);
vec3 eyeLightPos = LightPosition.xyz;
/* normal, tangent, and binormal in eye coordinates */
vec3 N = normalize(NormalMatrix*Normal);
vec3 T = normalize(NormalMatrix*objTangent);
vec3 B = cross(N, T);
/* light vector in texture space */
L.x = dot(T, eyeLightPos-eyePosition);
L.y = dot(B, eyeLightPos-eyePosition);
L.z = dot(N, eyeLightPos-eyePosition);
L = normalize(L);
/* view vector in texture space */
V.x = dot(T, -eyePosition);
V.y = dot(B, -eyePosition);
V.z = dot(N, -eyePosition);
V = normalize(V);
}
راهبرد Fragment Shader این است که Normalهای Perturbed و Normalizeشده را در قالب Normal Map از برنامه دریافت کند. Shader:
in vec3 L;
in vec3 V;
in vec2 st;
uniform vec4 DiffuseProduct;
uniform sampler2D texMap;
void main()
{
vec4 N = texture2D(texMap, st);
vec3 NN = normalize(2.0*N.xyz-1.0);
vec3 LL = normalize(L);
float Kd = max(dot(NN.xyz, LL), 0.0);
gl_FragColor = Kd*DiffuseProduct;
}
مقادیر Normal Map دوباره از بازهٔ (0.0,1.0) به (-1.0,1.0) Scale میشوند. DiffuseProduct برداری است که در برنامه محاسبه شده و هر مؤلفهٔ آن حاصلضرب مؤلفهٔ Diffuse نور در مؤلفهٔ Diffuse ماده است.
در این مثال View Vectorهای Texture Space که Vertex Shader تولید کرده استفاده نمیشوند؛ اگر بخواهیم مؤلفهٔ Specular اضافه کنیم به آنها نیاز خواهیم داشت. این تنها مقدمهای بر Bump Mapping است و کاربردهای بسیار قدرتمند آن اغلب در ترکیب با Procedural Texture Generation ظاهر میشوند.
۷٫۱۱ تکنیکهای Compositing
تا اینجا فرض میکردیم یک تصویر واحد میسازیم و سطح اشیای تشکیلدهندهٔ آن Opaque است. OpenGL از طریق Alpha (α) Blending سازوکاری دارد که از جمله میتواند اشیای Translucent بسازد. Alpha Channel چهارمین مؤلفه در حالت رنگ RGBA یا RGBα است. برنامه مقدار A یا α هر Pixel را مانند سایر مؤلفههای رنگ کنترل میکند؛ اما وقتی Blending فعال باشد، α تعیین میکند RGB چگونه در Frame Buffer نوشته شود.
چون Fragmentهای چند شیء میتوانند در رنگ یک Pixel نهایی سهم داشته باشند، میگوییم این اشیا با هم Blend یا Composite میشوند. همین سازوکار برای ترکیب چند Image نیز قابل استفاده است.
۷٫۱۱٫۱ Opacity و Blending
Opacity سطح میزان نوری است که از آن عبور نمیکند. Opacity برابر ۱ (α=1) سطح کاملاً Opaque را نشان میدهد که تمام نور ورودی را مسدود میکند. Opacity برابر ۰ یعنی سطح کاملاً Transparent است. Transparency/Translucency سطح با Opacity برابر α، مقدار (1-\alpha) است.
سه Polygon با نور یکنواخت را در شکل ۷٫۳۴ در نظر بگیرید. Polygon میانی Opaque و Polygon جلویی نزدیک بیننده Transparent فرض میشود. اگر Polygon جلو کاملاً Transparent باشد فقط Polygon میانی دیده میشود. اگر تا حدی Opaque باشد—مانند شیشهٔ رنگی—رنگ مشاهدهشده Blend رنگ Polygon جلو و وسط است. چون Polygon میانی Opaque است، Polygon عقب دیده نمیشود.
شکل ۷٫۳۴ — Polygonهای Translucent و Opaque.
اگر Polygon جلو قرمز و Polygon میانی آبی باشد، ترکیب آنها ارغوانی دیده میشود. اگر Polygon میانی نیز فقط تا حدی Opaque باشد، رنگ هر سه Polygon در نتیجه سهم خواهد داشت.
در گرافیک رایانهای معمولاً Polygonها یکییکی در Frame Buffer Render میشوند. بنابراین برای Blending/Compositing باید Opacity را در Fragment Processing وارد کنیم. مشابه Source و Destination Bitها، میتوان Source Pixel و Destination Pixel تعریف کرد. Fragment حاصل از Polygon، Source Pixel و Pixel موجود در Frame Buffer، Destination است؛ سپس این دو مقدار به روشهای مختلف ترکیب میشوند.
مقادیر α راهی برای کنترل Blending بهصورت Fragment-by-Fragment هستند. ترکیب رنگ چند Polygon شبیه چسباندن چند قطعه شیشهٔ رنگی است که قطعهٔ نهایی Opacity بیشتر و رنگی متفاوت از هر قطعهٔ اولیه دارد.
اگر Source و Destination را با آرایههای چهارعنصری RGBα نمایش دهیم:
s=[s_r\ s_g\ s_b\ s_a],\qquad d=[d_r\ d_g\ d_b\ d_a],
یک عمل Compositing مقدار d را با
d'=[b_rs_r+c_rd_r,\ b_gs_g+c_gd_g,\ b_bs_b+c_bd_b,\ b_as_a+c_ad_a]
جایگزین میکند. آرایههای
b=[b_r\ b_g\ b_b\ b_a],\qquad c=[c_r\ c_g\ c_b\ c_a]
بهترتیب Source Blending Factor و Destination Blending Factor هستند. مانند RGB، مقدار α بزرگتر از ۱ به ۱ و مقدار منفی به ۰ Clamp میشود. با انتخاب α و روش ترکیب Source/Destination میتوان افکتهای متنوعی ساخت.
۷٫۱۱٫۲ Image Compositing
سادهترین کاربرد Alpha Blending ترکیب و نمایش چند Image است که بهصورت Pixel Map یا مجموعهدادههای مستقلاً Renderشده وجود دارند. میتوان هر Image را شیئی تابشی دانست که در تصویر نهایی سهم مساوی دارد. معمولاً میخواهیم RGB تصویر نهایی در بازهٔ ۰ تا ۱ بماند، بدون نیاز به Clampکردن مقادیر بزرگتر از ۱. برای این کار یا مقادیر هر تصویر Scale میشوند یا از Blending Factorها استفاده میکنیم.
فرض کنید n تصویر باید سهم مساوی داشته باشند. در یک Pixel مشخص، تصویر i دارای مؤلفههای (C_i,\alpha_i) است و (C_i=(R_i,G_i,B_i)). اگر (C_i) را با (\frac1nC_i) و (\alpha_i) را با (\frac1n) جایگزین کنیم، میتوان همهٔ تصاویر را روی Frame Buffer سیاه اولیه با α=0 جمع کرد. روش جایگزین این است که α هر Pixel در همهٔ تصاویر را (1/n) بگذاریم، Destination Blending Factor را ۱ و Source Blending Factor را α بگیریم. هر دو روش تصویر یکسانی میسازند؛ اگر سختافزار Compositing را پشتیبانی کند، روش دوم میتواند کارآمدتر باشد. برای n بزرگ، ضرایب (1/n) ممکن است موجب کاهش Resolution رنگ شوند؛ Frame Bufferهای جدید با Floating-Point Arithmetic این مشکل را کاهش میدهند.
۷٫۱۱٫۳ Blending و Compositing در OpenGL
فعالکردن Blending:
و تعیین Source/Destination Factorها:
glBlendFunc(source_factor, destination_factor);
OpenGL ضرایب متعددی دارد؛ از جمله ۱ (GL_ONE)، صفر (GL_ZERO)، Source Alpha و مکمل آن (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) و Destination Alpha و مکمل آن (GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA). برنامه گزینهٔ مناسب را مشخص و سپس از رنگ RGBA استفاده میکند.
دشواری اصلی Compositing این است که برای بیشتر انتخابهای Blending Factor، ترتیب Rendering Polygonها نتیجهٔ نهایی را تغییر میدهد. برای مثال بسیاری از برنامهها Source α را Source Factor و (1-\alpha) را Destination Factor میگیرند. در این حالت رنگ جدید بهصورت Blend خطی Source و Destination محاسبه میشود و مقادیر رنگ/Opacity از Saturation دور میمانند، اما نتیجه به ترتیب رسم Polygonها وابسته است.
در نتیجه، برخلاف بیشتر برنامههای OpenGL که ترتیب Rasterizeشدن Polygonها اهمیت مستقیمی ندارد، هنگام Compositing باید برای رسیدن به افکت مطلوب این ترتیب در برنامه کنترل شود.
مسئلهٔ ظریفتر زمانی رخ میدهد که اشیای Opaque و Translucent در یک صحنه ترکیب شوند. معمولاً هنگام Blending، Hidden-Surface Removal را غیرفعال میکنیم، زیرا Polygonهایی که پشت Polygon قبلاً Renderشده قرار میگیرند ممکن است حذف شوند و دیگر در تصویر نهایی سهم نداشته باشند. اما در صحنهٔ ترکیبی، Polygon پشت یک سطح Opaque نباید دیده شود، در حالی که Polygon Translucent جلوی آن باید Composite شود.
راهحل ساده این است که Hidden-Surface Removal را عادی فعال نگه داریم، ولی هنگام Rendering Polygonهای Translucent، z-Buffer را فقطخواندنی کنیم:
در این حالت Polygon Translucent پشت یک Polygon Opaque قبلاً Renderشده حذف میشود. اگر جلوی Polygon موجود باشد، با رنگ آن Blend میشود، اما مقدار Depth در Buffer تغییر نمیکند. Polygonهای Opaque با Depth Mask فعال (GL_TRUE) بهطور عادی Render میشوند.
از آنجا که نتیجهٔ Compositing به ترتیب عناصر وابسته است، Rendering Polygonهای Translucent با ترتیب دلخواه ممکن است Artifact ایجاد کند. اگر بتوان Polygonهای Translucent را Sort کرد، روش مطمئن این است که نخست همهٔ Polygonهای Opaque Render شوند و سپس Polygonهای Translucent با ترتیب Back-to-Front و z-Buffer فقطخواندنی رسم گردند.
۷٫۱۱٫۴ بازگشت به Antialiasing
یکی از کاربردهای مهم Alpha Channel، Antialiasing است. یک Line برای دیدهشدن باید پهنای محدود داشته باشد و حداقل پهنای معمول آن یک Pixel است؛ خطی باریکتر از یک Pixel قابل نمایش مستقیم نیست. مگر اینکه Line کاملاً افقی یا عمودی باشد، بخشی از چند Pixel را در Frame Buffer میپوشاند، همانطور که در شکل ۷٫۳۵ دیده میشود.
شکل ۷٫۳۵ — Raster Line.
اگر در مرحلهٔ Geometry Processing برای هر Fragment مقدار α متناظر را برابر کسری از Pixel قرار دهیم که توسط Fragment پوشانده شده است، میتوان α را برای Modulateکردن رنگ هنگام نوشتن Fragment در Frame Buffer به کار برد. یک انتخاب، Destination Factor برابر (1-\alpha) و Source Factor برابر α است. بااینحال، وقتی Fragmentها درون یک Pixel همپوشانی داشته باشند، حالتهای متعددی ممکن است.
شکل ۷٫۳۶ — Fragmentها. (a) بدون همپوشانی. (b) دارای همپوشانی.
مسئله را از دید Rendererای در نظر بگیرید که Polygonها را یکییکی پردازش میکند. فرض کنید پسزمینه Opaque است و Frame Buffer ابتدا رنگ (C_0) را دارد. میتوان (\alpha_0=0) گرفت، چون هنوز بخشی از Pixel با Fragmentهای Polygon پوشانده نشده است.
پس از Rendering نخستین Polygon، رنگ Destination Pixel میشود:
C_d=(1-\alpha_1)C_0+\alpha_1C_1,
و Alpha آن:
\alpha_d=\alpha_1.
پس Fragmentی که کل Pixel را میپوشاند (α1=1) رنگ خود را مستقیماً به Destination میدهد و Pixel Opaque میشود. اگر پسزمینه سیاه باشد، رنگ Destination برابر (\alpha_1C_1) خواهد بود.
اکنون Fragment دوم را که همان Pixel را پوشش میدهد در نظر بگیرید. اگر Fragmentها همپوشانی نداشته باشند، رنگ جدید با Blend رنگ Destination و Fragment دوم به دست میآید:
C_d=(1-\alpha_2)((1-\alpha_1)C_0+\alpha_1C_1)+\alpha_2C_2,
\alpha_d=\alpha_1+\alpha_2.
این رنگ Blend دو Fragment است و به Clamp نیاز ندارد. α جدید کسری از Pixel را نشان میدهد که پوشیده شده است. بااینحال رنگ به ترتیب Rendering Polygonها وابسته است.
اگر Fragmentها همپوشانی داشته باشند، هم تعیین مقدار و هم تشخیص همپوشانی دشوارتر است. یک رویکرد، تفسیر احتمالاتی است: اگر Fragment اول کسری (\alpha_1) و Fragment دوم کسری (\alpha_2) از Pixel را بپوشانند و اطلاعات دیگری از موقعیت داخل Pixel نداشته باشیم، میانگین مساحت همپوشانی (\alpha_1\alpha_2) است. بنابراین:
\alpha_d=\alpha_1+\alpha_2-\alpha_1\alpha_2.
شکل ۷٫۳۷ — همپوشانی متوسط.
تعیین رنگ پیچیدهتر است، زیرا باید بدانیم Fragment دوم جلوی اول است، اول جلوی دوم است، یا هر دو باید با هم Blend شوند. برای هر فرض میتوان Blending مناسب تعریف کرد. در Pipeline Renderer، Polygonها ممکن است به ترتیبی تولید شوند که هیچ ارتباطی با فاصلهٔ آنها از بیننده ندارد. ترکیب α Blending و Hidden-Surface Removal اجازه میدهد از Depth برای تصمیم Front/Back استفاده شود.
در OpenGL میتوان بدون ترکیب صریح α در برنامه، Antialiasing Line و Polygon را با فعالکردن Smoothing و Blending انجام داد:
glEnable(GL_LINE_SMOOTH);
glEnable(GL_POLYGON_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Antialiasing ممکن است هزینهٔ عملکرد قابلتوجهی داشته باشد.
۷٫۱۱٫۵ Rendering از Back-to-Front و Front-to-Back
Alpha Channel راهی برای ایجاد ظاهر Translucency میدهد، اما پیادهسازی Transparency بهشکل فیزیکی صحیح بدون توجه به نورپردازی جسم و رفتار Rayها هنگام عبور از سطح Translucent دشوار است.
شکل ۷٫۳۸ برخی پیچیدگیها را نشان میدهد. Refraction نور از سطوح Translucent را نادیده میگیریم، زیرا Pipeline Polygon Renderer بهسادگی از عهدهٔ آن برنمیآید. فرض کنید Polygon عقب Opaque ولی Reflective است و دو Polygon نزدیکتر به Viewer Translucent هستند. Rayهای مختلف نور ممکن است مستقیماً به Polygon عقب برسند یا پیش از آن از یک یا دو سطح Translucent عبور کنند.
شکل ۷٫۳۸ — صحنهای با اشیای Translucent.
برای نقطههایی که نور مستقیم به Polygon عقب میرسد، Pixel بر اساس Shade همان نقطه محاسبه میشود. اگر نور ورودی از یک سطح Translucent عبور کرده باشد، رنگ باید بر اساس رنگ و Opacity آن سطح تعدیل شود و همچنین مؤلفهای برای نوری که از Polygon جلویی به سمت Viewer بازتاب شده در نظر گرفته شود. Rayهایی که از دو سطح Translucent عبور میکنند به اثر ترکیبی هر دو نیاز دارند.
برای Pipeline Renderer مسئله دشوارتر—و گاهی عملاً غیرممکن—است، چون سهم هر Polygon باید هنگام عبور همان Polygon از Pipeline تعیین شود، نه زمانی که همهٔ Polygonهای مؤثر بر یک Pixel همزمان در اختیار داریم. در کاربردهایی که Translucency باید سازگار و واقعگرایانه باشد، معمولاً Polygonها در برنامه بر اساس فاصله Sort میشوند و سپس با قابلیت Blending در OpenGL بهصورت Front-to-Back یا Back-to-Front Render میشوند.
۷٫۱۱٫۶ Scene Antialiasing و Multisampling
بهجای Antialiasing جداگانهٔ Lineها و Polygonها میتوان کل صحنه را با Multisampling Antialias کرد. در این حالت هر Pixel در Frame Buffer شامل چند Sample است و هر Sample میتواند Color، Depth و مقادیر دیگر را نگهداری کند. صحنه گویی با Resolution بالاتر Render میشود، اما هنگام نمایش، Sampleهای هر Pixel ترکیب میشوند تا رنگ نهایی Pixel به دست آید.
در OpenGL تعداد Sampleهای هر Pixel هنگام ایجاد Frame Buffer تعیین میشود. در برنامههای مبتنی بر GLUT میتوان گزینهٔ GLUT_MULTISAMPLE را به glutInitDisplayMode افزود تا Frame Buffer دارای چند Sample در هر Pixel باشد.
مانند Line/Polygon Antialiasing، Multisampling نیز در طول Rendering قابل فعال/غیرفعالکردن است:
glEnable(GL_MULTISAMPLE);
و برای توقف:
glDisable(GL_MULTISAMPLE);
در عمل برنامه معمولاً یا در تمام زمان Rendering از Multisampling استفاده میکند یا اصلاً از آن استفاده نمیکند.
۷٫۱۱٫۷ Image Processing
Pixel Mapping را میتوان برای عملیات مختلف Image Processing به کار برد. فرض کنید با یک Image گسسته آغاز میکنیم؛ این Image ممکن است با Rendering تولید شده یا از Digitizeکردن تصویر پیوسته توسط Scanner به دست آمده باشد. Image را با ماتریس (N\times M) زیر از سطوح Scalar نمایش میدهیم:
A=[a_{ij}].
اگر هر Color Component یک Image رنگی جداگانه پردازش شود، عناصر A میتوانند مؤلفههای منفرد رنگ یا سطوح Gray/Luminance باشند. یک Linear Filter ماتریس Filterشدهٔ B را میسازد:
b_{ij}=\sum_{k=-m}^{m}\sum_{l=-n}^{n}h_{kl}a_{i+k,j+l}.
میگوییم B حاصل Convolution ماتریس A با Filter Matrix یعنی H است. معمولاً m و n کوچکاند و H به شکل یک Convolution Matrix کوچک با اندازهٔ ((2m+1)\times(2n+1)) نمایش داده میشود.
شکل ۷٫۳۹ — Filtering و Convolution.
برای m=n=1، Convolution Matrix روی هر (a_{ij}) قرار میگیرد و میانگین وزندار نقاط اطراف محاسبه میشود. وزنها همان عناصر ماتریساند. برای نمونه، میانگین Pixel با چهار همسایهٔ مستقیم با Kernel زیر:
H=\frac15\begin{bmatrix}0&1&0\\1&1&1\\0&1&0\end{bmatrix}.
این Filter برای Antialiasing قابل استفاده است. میتوان تعداد نقاط را بیشتر و وزن مرکز را بالاتر گرفت:
H=\frac1{16}\begin{bmatrix}1&2&1\\2&4&2\\1&2&1\end{bmatrix}.
اگر بخواهیم B همان ابعاد A را داشته باشد، باید پیرامون A یک Border تعریف کنیم. با Matrixهای کوچک عملیات دیگری نیز ممکن است. برای مثال Kernel
H=\begin{bmatrix}0&-1&0\\-1&4&-1\\0&-1&0\end{bmatrix}
برای تشخیص تغییرات مقدار یا Edgeهای Image قابل استفاده است. اگر H اندازهٔ (k\times k) داشته باشد، Filter را میتوان با انباشتن (k^2) تصویر در Frame Buffer پیاده کرد؛ هر بار نسخهای Shiftشده از A با وزن مناسب اضافه میشود.
۷٫۱۱٫۸ سایر روشهای Multipass
Blending را میتوان برای Filtering در زمان و عمق نیز به کار برد. اگر یک شیء را اندکی Jitter کنیم و چند بار Render نماییم، در حالی که موقعیت اشیای دیگر ثابت باشد، نسخههای کمرنگتری از شیء Jitterشده در تصویر نهایی باقی میمانند. اگر شیء بهجای Jitter تصادفی روی یک مسیر حرکت کند، رد حرکت آن دیده میشود. این Motion Blur مشابه عکسبرداری از جسم متحرک با زمان نوردهی طولانی است. با تنظیم α میتوان موقعیت نهایی را Opaqueتر Render کرد یا تفاوت سرعت را القا نمود.
Filtering در Depth نیز برای ساخت افکت Focus به کار میرود. دوربین واقعی نمیتواند همهٔ اشیای صحنه را همزمان کاملاً در Focus نگه دارد. اشیای واقع در محدودهای از فاصله، یعنی Depth of Field دوربین، واضحاند و اشیای بیرون آن Blur میشوند. تصاویر گرافیک رایانهای بهطور طبیعی Depth of Field نامحدود دارند، زیرا محدودیت Lens واقعی وجود ندارد؛ اما گاهی برای ظاهر عکاسانه یا تأکید بر بخشی از صحنه میخواهیم عمق میدان محدود شبیهسازی شود.
ترفند این است که Viewer را طوری جابهجا کنیم که یک Plane خاص ثابت بماند، همانطور که شکل ۷٫۴۰ نشان میدهد. فرض کنید صفحهٔ (z=z_f) باید در Focus باقی بماند و Near/Far Clipping Distanceهای (z_{min}) و (z_{max}) تغییر نکنند. هنگام استفاده از Frustum، Near Clipping Rectangle با (xmin, xmax, ymin, ymax) مشخص میشود. سپس با Jitterکردن موقعیت Viewer و اصلاح متناظر Frustum میتوان چند تصویر تولید و Blend کرد؛ نقاط روی صفحهٔ Focus ثابت میمانند و نقاط بیرون آن در نمونهها جابهجا شده و پس از ترکیب Blur دیده میشوند.
شکل ۷٫۴۰ — Jitter برای شبیهسازی Depth of Field.