حذف سطح پنهان، نمایش مش، سایهها و تمرینهای فصل چهارم
4.8 حذف سطح پنهان
از دید گرافیک رایانهای، هر شش وجه مکعب تعریف شده و از خط لولهٔ گرافیکی عبور میکنند؛ بنابراین سامانه باید تعیین کند کدام سطوح واقعاً نمایش داده شوند. در سطح مفهومی به دنبال الگوریتمهایی هستیم که یا سطوحی را که نباید برای بیننده قابل مشاهده باشند حذف کنند ــ الگوریتمهای حذف سطح پنهان ــ یا مستقیماً سطوح قابل مشاهده را پیدا کنند ــ الگوریتمهای سطح قابل مشاهده. روشهای گوناگونی وجود دارد که چند مورد در فصل ۶ بررسی میشوند. OpenGL با الگوریتم مشخصی به نام z-buffer کار میکند و از طریق چند فراخوانی تابع میتوان آن را فعال کرد؛ بنابراین در اینجا آن را معرفی و در فصل ۶ دوباره با جزئیات بررسی میکنیم.
الگوریتمهای حذف سطح پنهان را میتوان به دو گروه اصلی تقسیم کرد. الگوریتمهای فضای شیء میکوشند سطوح اشیای صحنه را چنان مرتب کنند که رندر آنها با یک ترتیب خاص تصویر درست را بسازد. برای مثال، در مکعب اگر ابتدا سطوح پشترو را رندر کنیم و سپس سطوح جلو را روی آنها «نقاشی» کنیم، تصویر درست حاصل میشود. این دسته با معماریهای خط لولهای، که اشیا در ترتیب دلخواه وارد خط لوله میشوند، سازگاری خوبی ندارد؛ برای تعیین ترتیب مناسب، سامانه باید همهٔ اشیا را همزمان در اختیار داشته باشد تا آنها را از عقب به جلو مرتب کند.
الگوریتمهای فضای تصویر بخشی از فرایند projection هستند و رابطهٔ نقاط اشیا را روی هر Projector تعیین میکنند. الگوریتم z-buffer از این نوع است و با خط لولهٔ رندر سازگاری خوبی دارد، زیرا هنگام رندر هر شیء میتوان اطلاعات جزئی را ذخیره و بهروزرسانی کرد.
ایدهٔ اصلی z-buffer در شکل 4.41 آمده است. Projectorی از COP از دو سطح عبور میکند. چون دایره به بیننده نزدیکتر از مثلث است، رنگ دایره باید رنگ محل متناظر در Color Buffer را تعیین کند. مسئله آن است که این نتیجه مستقل از ترتیب ورود مثلث و دایره به خط لوله حاصل شود.
شکل 4.41 ــ الگوریتم z-buffer.
فرض کنید همهٔ اشیا چندضلعیاند. اگر هنگام Rasterize شدن چندضلعیها بتوانیم فاصلهٔ نزدیکترین نقطهٔ رندرشده روی هر Projector را از COP یا صفحهٔ projection نگه داریم، با ورود چندضلعیهای بعدی این اطلاعات را بهروزرسانی میکنیم و در پایان فقط نزدیکترین نقطهٔ هر Projector نمایش داده میشود. الگوریتم به Depth Buffer یا z-buffer نیاز دارد تا اطلاعات عمق هنگام Rasterization ذخیره شوند. چون برای هر Pixel در Color Buffer به عمق متناظر نیاز داریم، تفکیک مکانی z-buffer با Color Buffer برابر است. در کارتهای گرافیکی جدید، عمق معمولاً با ۳۲ بیت و اغلب به صورت Floating Point ذخیره میشود. z-buffer یکی از Bufferهای Frame Buffer و معمولاً بخشی از حافظهٔ کارت گرافیک است.
Depth Buffer ابتدا با مقداری متناظر با دورترین فاصله از بیننده مقداردهی میشود. هنگام Rasterize شدن هر چندضلعی داخل حجم برش، عمق هر Fragment محاسبه میشود. اگر عمق Fragment از مقدار ذخیرهشده در محل متناظر Depth Buffer بیشتر باشد، یعنی سطحی که پیشتر رندر شده روی همان Projector به بیننده نزدیکتر بوده است؛ بنابراین رنگ Fragment جدید نادیده گرفته میشود. اگر عمق کمتر باشد، چندضلعی جدید نزدیکترین سطح دیدهشده تا این لحظه است؛ پس Color Buffer با رنگ آن و z-buffer با عمق جدید بهروزرسانی میشود.⁶
در مثال شکل 4.41، اگر مثلث اول وارد خط لوله شود، رنگ و عمق آن در Bufferها قرار میگیرد و سپس دایره در ناحیهٔ همپوشانی آنها را جایگزین میکند. اگر دایره اول رندر شود، هنگام رسیدن مثلث عمق آن در ناحیهٔ همپوشانی بزرگتر است و بنابراین هیچ تغییری در Color/Depth Buffer رخ نمیدهد.
مزیت مهم این الگوریتم آن است که پیچیدگی آن متناسب با تعداد Fragmentهای تولیدشده توسط Rasterizer است و تنها محاسبات اضافی کمی نسبت به رندر معمولی چندضلعیها لازم دارد.
از دید برنامهنویس کاربردی، Depth Buffer باید هنگام ایجاد پنجره درخواست و سپس آزمون عمق فعال شود:
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glEnable(GL_DEPTH_TEST);
⁶ رنگ چندضلعی در صورت فعالبودن قابلیتها از Shading (فصل ۵) و Texture Mapping (فصل ۷) تأثیر میگیرد.
برای شروع یک رندر جدید میتوان Color Buffer و Depth Buffer را همزمان پاک کرد:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
4.8.1 Culling
برای یک جسم محدب مانند مکعب، وجههایی که Normal آنها از بیننده دور است هرگز دیده نمیشوند و میتوان پیش از Rasterizer آنها را حذف یا Cull کرد. در OpenGL:
با این حال Culling فقط برای یک جسم محدب تضمین میکند تصویر صحیح باقی بماند. اغلب میتوان آن را در کنار z-buffer به کار برد. برای مثال، در صحنهای شامل (n) مکعب، بدون Culling تعداد (6n) چندضلعی از کل خط لوله عبور میکنند. با Culling تقریباً نیمی از آنها در مراحل ابتدایی حذف میشوند و فقط (3n) چندضلعی همهٔ مراحل را طی میکنند. این موضوع در فصل ۶ بیشتر بررسی میشود.
4.9 نمایش Meshها
اکنون ابزار لازم برای حرکت تعاملی در صحنه با تغییر پارامترهای دوربین در پاسخ به ورودی کاربر را داریم. پیش از معرفی رابط ساده، نمونهٔ دیگری از نمایش داده را بررسی میکنیم: Mesh Plot.
Mesh مجموعهای از چندضلعیهاست که رأسها و Edgeهای مشترک دارند. Mesh عمومی ممکن است چندضلعیهایی با هر تعداد رأس داشته باشد و برای ذخیره و نمایش کارآمد به ساختار داده نسبتاً پیچیدهای نیاز دارد. Meshهای مستطیلی و مثلثی، مانند Mesh مورد استفاده در فصل ۲ برای مدلسازی کره، بسیار سادهترند و در کاربردهای فراوان مفید واقع میشوند. در اینجا Mesh مستطیلی را برای نمایش دادههای ارتفاع معرفی میکنیم.
شکل 4.42 ــ Mesh.
دادهٔ ارتفاع سطحی مانند Terrain را با تابع یا نمونههای اندازهگیریشده توصیف میکند. فرض کنید ارتفاع (y) با تابع
y=f(x,z)
داده شود؛ برای هر ((x,z)) دقیقاً یک (y) داریم. چنین سطحهایی گاه سطوح دوونیمبعدی یا Height Field نامیده میشوند. همهٔ سطوح را نمیتوان چنین نمایش داد، اما این مدل در کاربردهایی مانند نمایش ارتفاع از سطح دریا بسیار مفید است.
شکل 4.43 ــ Height Field.
در بسیاری از مسائل تابع (f) فقط به صورت گسسته شناخته شده و نمونههای داده داریم:
y_{ij}=f(x_i,z_j).
فرض میکنیم نقاط با فاصلهٔ یکنواخت قرار دارند:
x_i=x_0+i\Delta x,\quad i=0,\ldots,N,
z_j=z_0+j\Delta z,\quad j=0,\ldots,M.
اگر (f) تحلیلی باشد نیز میتوان آن را Sample کرد و دادهٔ گسسته ساخت.
سادهترین روش نمایش داده این است که برای هر (x) یک Line Strip و برای هر (z) یک Line Strip رسم کنیم و در مجموع (N+M) نوار خطی بسازیم. اگر دادههای ارتفاع در آرایهٔ data باشند، آرایهٔ رأسهای مرتبشده بر اساس سطر را میتوان چنین ساخت:
float data[N][M];
point4 vertices[2*N*M];
int k =0;
for(int i = 0; i<N; i++) for(int j=0; j<M; j++)
{
vertices[k] = vec4(i, data[i][j], j, 1.0);
k++;
}
برای تشکیل آرایه بر اساس ستون، نقش i و j عوض میشود.
point4 vertices[N*M];
int k =0;
for(int i = 0; i<M; i++) for(int j=0; j<N; j++)
{
vertices[k] = point4(j, data[j][i],i,1.0);
k++;
}
معمولاً دادهها را به بازهای مناسب مانند ((0,1)) مقیاس میکنیم و مقادیر (x,z) را نیز با model-view یا تنظیم حجم دید برای نمایش بهتر تغییر مقیاس میدهیم.
در Initialization، Vertex Buffer Object را با اندازهٔ مناسب ولی بدون ارسال داده میسازیم:
GLuint buffer;
glBindVertexArray(buffer);
loc = glGetAttribLocation(program, "vPosition");
glEnableVertexAttribArray(loc);
glVertexAttribPointer(loc, 4, GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET(0));
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), NULL,
GL_DYNAMIC_DRAW);
در Callback نمایش، دو آرایهٔ رأس را بهترتیب بارگذاری و رسم میکنیم:
/* form array of vertices by row here */
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices,
GL_DYNAMIC_DRAW);
glDrawArrays(GL_LINE_STRIP, 0, N*M);
/* form array of vertices by column here */
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices,
GL_DYNAMIC_DRAW);
glDrawArrays(GL_LINE_STRIP, 0, N*M);
شکل 4.44 Mesh مستطیلی حاصل از دادههای ارتفاع بخشی از Honolulu در Hawaii را نشان میدهد.
شکل 4.44 ــ Mesh Plot دادههای Honolulu با Line Stripها.
این روش ساده سه مشکل دارد. نخست، برای هر نمایش باید داده را دو بار روی GPU بارگذاری کنیم و زمان زیادی صرف جابهجایی CPU→GPU میشود. دوم، حذف سطح پنهان نداریم و Lineهای بخشهایی که باید پشت سطح باشند دیده میشوند. سوم، Lineهای اضافی مزاحمی از انتهای یک سطر یا ستون به ابتدای بعدی ایجاد میشوند، زیرا همهٔ سطرها و ستونها در یک Line Strip قرار گرفتهاند.
مشکل سوم را میتوان با انتخاب دقیق رنگ رأسها کاهش داد. دو مشکل نخست را میتوان با نمایش داده به صورت سطح چندضلعی برطرف کرد؛ مزیت دیگر آن است که در فصل ۵ بهسادگی نور و خواص ماده را نیز اضافه خواهیم کرد.
4.9.1 نمایش Mesh به صورت سطح
یک روش ساده برای تولید سطح، استفاده از Mesh مثلثی است. چهار نمونهٔ (y_{ij})، (y_{i+1,j})، (y_{i+1,j+1}) و (y_{i,j+1}) دو مثلث میسازند. بنابراین دادهٔ ارتفاع یک Mesh با حدود (2NM) مثلث تعیین میکند.
کد زیر داده را در بازهٔ ((0,1))، مقادیر (x) را در ((-1,1)) و (z) را در ((-1,0)) نرمال میکند و آرایهٔ مثلثها را میسازد:
float data[N][M]; // all values assumed non negative
float fmax; // maximum of data
point4 triangles[6*N*M]; // vertex positions
float fn = float(N);
float fm = float(M);
int k =0;
for(i=0; i<N-1; i++) for(j=0; j<M-1;j++)
{
triangles[k] = vec4(2.0*(i/fn-0.5), data[i][j]/fmax, -j/fm,
1.0); k++;
triangles[k] = vec4(2.0*((i+1)/fn-0.5), data[i+1][j]/fmax,
-j/fm, 1.0); k++;
triangles[k] = vec4(2.0*((i+1)/fn-0.5), data[i+1][j+1]/fmax,
-(j+1)/fm,1.0); k++;
triangles[k] = vec4(2.0*((i+1)/fn-0.5),data[i+1][j]/fmax,
-j/fm, 1.0); k++;
triangles[k] = vec4(2.0*((i+1)/fn-0.5), data[i+1][j+1]/fmax,
-(j+1)/fm,1.0); k++;
triangles[k] = vec4(2.0*(i/fn-0.5), data[i][j+1]/fmax,
-(j+1)/fm, 1.0); k++;
}
Vertex Array مانند قبل آماده میشود:
glBindVertexArray(abuffer);
loc = glGetAttribLocation(program, "vPosition");
glEnableVertexAttribArray(loc);
glVertexAttribPointer(loc, 4, GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET(0));
glGenBuffers(1, buffers);
glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangles), triangles,
GL_STATIC_DRAW);
و رسم اولیه:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glDrawArrays(GL_TRIANGLES, 0, 6*N*M);
اگر فقط Edgeها را با GL_LINE رسم کنیم Fragment داخلی تولید نمیشود، پس Edgeهای پنهان همچنان دیده میشوند. میتوان سطح را دو بار رندر کرد: بار نخست سطح پرشدهٔ سفید و بار دوم Lineهای سیاه. چون داده از قبل روی GPU است، برای رندر دوم انتقال دادهای لازم نیست.
color4 white = vec4(1.0, 1.0, 1.0, 1.0);
color4 black = vec4(0.0, 0.0, 0.0, 1.0);
color_loc = glGetUniformLocation(program, "fcolor");
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glUniform4fv(color_loc, 1, white);
glDrawArrays(GL_TRIANGLES, 0, 6*N*M);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glUniform4fv(color_loc, 1, black);
glDrawArrays(GL_TRIANGLES, 0, 6*N*M);
Fragment Shader اصلاحشده:
uniform vec4 fcolor;
void main()
{
gl_FragColor = fcolor;
}
4.9.2 Polygon Offset
اگر همهٔ دادهها استفاده شوند، Plot ممکن است تعداد بسیار زیادی چندضلعی کوچک و تراکم Line بالا یا الگوهای Moiré داشته باشد. میتوان داده را زیرنمونهبرداری کرد؛ مثلاً هر نقطهٔ (k)-ام را گرفت یا گروهی از نمونهها را میانگین گرفت تا (N,M) کوچکتر شوند.
مشکل دیگر همصفحهبودن سطح پرشده و Edgeهای رسمشده روی آن است. خطاهای عددی Renderer ممکن است بخشی از رندر دوم را پشت Fragmentهای رندر نخست قرار دهند. با Polygon Offset میتوان سطح پرشده را اندکی از بیننده دور کرد تا Lineها واضح بمانند:
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(1.0, 1.0);
دو پارامتر PolygonOffset با شیب چندضلعی و یک ثابت وابسته به پیادهسازی ترکیب میشوند؛ بنابراین ممکن است برای یافتن مقدار مناسب نیاز به آزمون باشد.
ضعف بزرگتر کد این است که دادهٔ زیادی به GPU میفرستیم و از روش رندر کارآمد استفاده نمیکنیم. برای یک ردیف (N) چهارضلعی، اگر آنها را به صورت (2N) مثلث مجزا بفرستیم به (6N) رأس نیاز داریم.
point4 vertices[6*N];
glDrawArrays(GL_TRIANGLES, 0, 6*N);
اگر به جای آن یک Triangle Strip بسازیم:
point4 vertices[2*N-2];
glDrawArrays(GL_TRIANGLE_STRIP, 0, 2*N-2);
هم داده و حافظهٔ کمتری مصرف میشود و هم GPU Triangle Strip را سریعتر از مثلثهای منفرد رندر میکند. ساخت Strip برای Mesh یکبعدی ساده است، اما برای (N\times M) پیچیدهتر میشود. ارسال (N) Strip جداگانه دوباره انتقالهای متعدد داده ایجاد میکند؛ بنابراین بهتر است یک Triangle Strip واحد برای کل Mesh بسازیم. تمرینهای 4.22 و 4.23 دو رویکرد پیشنهاد میدهند.
4.9.3 حرکت درون صحنه
گام بعد تعیین دوربین و افزودن تعامل است. در این نسخه از مشاهدهٔ پرسپکتیو استفاده میکنیم و با کلیدهای x/X/y/Y/z/Z جایگاه دوربین را تغییر میدهیم، در حالی که دوربین همیشه به مرکز مکعب نگاه میکند. LookAt برای بازجایگذاری و جهتدهی دوربین مناسب است.
آرایهٔ viewer[3] موقعیت دوربین را نگه میدارد و Callback صفحهکلید آن را تغییر میدهد:
void keys(unsigned char key, int x, int y)
{
if(key == 'x') viewer[0]-= 1.0;
if(key == 'X') viewer[0]+= 1.0;
if(key == 'y') viewer[1]-= 1.0;
if(key == 'Y') viewer[1]+= 1.0;
if(key == 'z') viewer[2]-= 1.0;
if(key == 'Z') viewer[2]+= 1.0;
glutPostRedisplay();
}
تابع نمایش LookAt را با viewer به عنوان eye و مبدأ به عنوان at فراخوانی میکند. مکعب نیز مانند قبل بر اساس ورودی ماوس دوران میکند.
mat4 model_view;
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
model_view = LookAt(viewer[0],viewer[1],viewer[2],
0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
*RotateX(theta[0])*RotateY(theta[1])*RotateZ(theta[2]);
/* draw mesh or other objects here */
glutSwapBuffers();
}
در Callback تغییر اندازهٔ پنجره، Frustum لنز دوربین را بر اساس Aspect Ratio تنظیم میکند:
mat4 projection;
void myReshape(int w, int h)
{
glViewport(0, 0, w, h);
GLfloat left = -2.0, right = 2.0, bottom = -2.0, top = 2.0;
GLfloat aspect = (GLfloat) w / h;
if ( aspect <= 1.0 ) {
bottom /= aspect;
top /= aspect;
} else {
left *= aspect;
right *= aspect;
}
projection = Frustum(left, right, bottom, top, 2.0, 20.0);
glUniformMatrix4fv(projection_loc, 1, GL_TRUE, projection);
}
پارامترهای Frustum بر اساس نسبت ابعاد پنجره انتخاب میشوند تا تغییر شکل نامطلوب کاهش یابد. با اجرای برنامه میتوان اثر حرکت دوربین، تغییر لنز و تغییر اضلاع Frustum را دید. هنگام نزدیکشدن به Mesh نیز باید به اثر این تغییرات و این واقعیت توجه کرد که دوربین همیشه به مرکز Mesh نگاه میکند.
میتوان از دکمههای ماوس نیز برای حرکت بیننده استفاده کرد، مثلاً حرکت رو به جلو یا چرخش ۹۰ درجه به راست و چپ؛ اما در این مثال صفحهکلید برای حرکت بیننده باقی میماند تا ماوس برای حرکت خود شیء آزاد باشد.
این مثال از جایگذاری مستقیم دوربین با LookAt استفاده میکند. روش دیگر تغییر افزایشی model-view با ماتریسهای دوران و انتقال است. اگر بخواهیم بیننده بدون نگاه دائمی به یک نقطهٔ ثابت حرکت کند، این گزینه میتواند طبیعیتر باشد. همچنین میتوان متغیر جایگاه را نگه داشت و در هر گام model-view را از ابتدا محاسبه کرد. انتخاب روش به کاربرد بستگی دارد؛ تغییر افزایشی بسیار طولانی ممکن است خطاهای عددی را انباشته کند.
رندر Mesh را میتوان در فصلهای بعد گسترش داد: فصل ۵ نور و خواص سطح را اضافه میکند، فصل ۷ Texture روی سطح قرار میدهد و فصل ۱۰ امکان تعریف سطوح نرمتر از داده را فراهم میکند. با ترکیب این روشها میتوان Terrain واقعگرایانه ساخت و حتی با تغییر جایگاه منبع نور اثر زمان روز را شبیهسازی کرد.
4.10 تصویرسازی و سایهها
ساخت سایههای ساده کاربرد جالبی از ماتریسهای projection است. سایهها شیء هندسی مستقل نیستند، اما در واقعگرایی و درک رابطهٔ فضایی اشیا بسیار مهماند. از دید فیزیکی، سایه به منبع نور نیاز دارد. نقطهای در سایه است اگر هیچ منبع نوری آن را روشن نکند یا، بهطور معادل، ناظری در آن نقطه هیچ منبع نوری را نبیند. اگر تنها منبع نور دقیقاً در COP باشد، سایهٔ قابل مشاهدهای نداریم، زیرا سایهها پشت اشیای دیدهشده قرار میگیرند. این راهبرد نورپردازی «چراغقوه در چشم» نامیده شده است.
برای سایههای فیزیکی صحیح باید تعامل نور و ماده را بدانیم؛ موضوع فصل ۵. محاسبات Global Lighting دشوارند و معمولاً به صورت Real-time انجام نمیشوند. با این حال اهمیت سایهها در کاربردهایی مانند شبیهساز پرواز باعث توسعهٔ روشهای ویژهای شده است.
سایهٔ ایجادشده توسط یک منبع نقطهای را فرض کنید که روی سطح زمین
y=0
میافتد.
شکل 4.45 ــ سایه از یک چندضلعی منفرد.
سایه یک چندضلعی تخت، موسوم به Shadow Polygon، است و در واقع projection چندضلعی اصلی روی سطح محسوب میشود؛ COP این projection همان منبع نور است. اگر در Frameای که منبع نور در مبدأ است چندضلعی را روی صفحهٔ سطح تصویر کنیم، رأسهای Shadow Polygon به دست میآیند و سپس باید به Frame شیء بازگردانده شوند. به جای انجام این کار به طور دستی در برنامه میتوان ماتریس projection مناسب ساخت.
منبع نور در ((x_l,y_l,z_l)) را در نظر بگیرید. ابتدا با
T(-x_l,-y_l,-z_l)
منبع را به مبدأ میبریم. سپس یک تصویرسازی پرسپکتیو ساده از مبدأ انجام میدهیم که ماتریس آن برای صفحهٔ (y=0) به شکل مناسب ساخته میشود. در پایان با
T(x_l,y_l,z_l)
همه چیز را به Frame اصلی بازمیگردانیم. تبدیل الحاقی رأس ((x,y,z)) را به:
x_p=x_l-\frac{x-x_l}{(y-y_l)/y_l},
y_p=0,
z_p=z_l-\frac{z-z_l}{(y-y_l)/y_l}
نگاشت میکند.
شکل 4.46 ــ projection چندضلعی سایه: (الف) از منبع نور، (ب) پس از انتقال منبع به مبدأ.
در OpenGL میتوان model-view را برای ساخت چندضلعی سایه تغییر داد. اگر منبع نور ثابت باشد، projection سایه یک بار در Initialization محاسبه میشود؛ اگر نور حرکت کند باید ماتریس در طول اجرا دوباره محاسبه شود.
float light[3]; // location of light
mat4 m; // shadow projection matrix initially an identity matrix
m[3][1] = -1.0/light[1];
برای یک مربع موازی صفحهٔ (y=0):
point4 square[4] = {
vec4(-0.5, 0.5, -0.5, 1.0), vec4(-0.5, 0.5, 0.5, 1.0),
vec4(0.5, 0.5, -0.5, 1.0), vec4(0.5, 0.5, 0.5, 1.0)
};
ترتیب رأسها اجازه میدهد مربع با Triangle Strip رندر شود. برای مربع رنگ قرمز و برای سایه رنگ سیاه به Fragment Shader میفرستیم و Vertex Array/Buffer را مانند مثالهای قبل مقداردهی میکنیم.
GLuint abuffer, buffer;
glGenVertexArrays(1, &abuffer);
glBindVertexArray(abuffer);
int loc = glGetAttribLocation(program, "vPosition");
glEnableVertexAttribArray(loc);
glVertexAttribPointer(loc, 4, GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET(0));
color_loc = glGetUniformLocation(program, "fcolor");
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(square), square,
GL_STATIC_DRAW);
اگر داده ثابت است، projection و model-view را نیز میتوان در Initialization ساخت و به Vertex Shader فرستاد:
model_view = LookAt(eye, at, up);
projection = Ortho(left, right, bottom, top, near, far);
glUniformMatrix4fv(matrix_loc, 1, GL_TRUE, model_view);
glUniformMatrix4fv(projection_loc, 1, GL_TRUE, projection);
هستهٔ Callback نمایش:
void display()
{
mat4 mm;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// render red square
glUniform4fv(color_loc, 1, red);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// matrix to compute vertices of shadow polygon
mm = model_view*Translate(light[0], light[1], light[2])
*m*Translate(-light[0], -light[1], -light[2]);
glUniformMatrix4fv(matrix_loc, 1, GL_TRUE, mm);
// render shadow polygon
glUniform4fv(color_loc, 1, black);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glutSwapBuffers();
}
با آنکه projection از دید منبع نور انجام میشود، ماتریسی که در برنامه تغییر میدهیم model-view است. همان چندضلعی را دوبار رندر میکنیم: بار نخست معمولی و بار دوم با model-view تغییریافته که رأسها را به Shadow Polygon تبدیل میکند. شرایط مشاهدهٔ یکسان روی خود چندضلعی و سایه اعمال میشوند.
برای محیط سادهای مانند هواپیما روی Terrain تخت که فقط یک سایه میاندازد، این روش مناسب است. تبدیل منبع نقطهای به منبع دوردست و موازی نیز ساده است. اما وقتی اشیا بتوانند روی یکدیگر سایه بیندازند، این روش عملی نیست. فصل ۱۱ روشهای عمومیتر اما کندتری را بررسی میکند که سایه را به عنوان بخشی از فرایند رندر تولید میکنند.
جمعبندی و یادداشتها
اکنون میتوانیم برنامههای سهبعدی کامل و غیرساده بنویسیم. تمرین عملی ساخت چنین برنامهای بهترین راه برای کسب مهارت در دستکاری توابع model-view و projection است.
ریاضیات تصویرسازیهای استاندارد را ارائه کردیم. اگرچه بیشتر APIها برنامهنویس را از نوشتن دستی توابع projection بینیاز میکنند، درک ریاضیات کمک میکند پیادهسازی خط لولهای مبتنی بر الحاق ماتریسهای (4\times4) را بفهمیم. تا سالهای نهچندان دور، projection باید در خود برنامه انجام میشد و بسیاری از سختافزارها حتی پرسپکتیو را پشتیبانی نمیکردند.
سه محور اصلی در ادامهٔ کتاب وجود دارد. نخست، مدلسازی را با Primitiveهای بیشتر گسترش میدهیم. فصل ۸ روابط پیچیدهتر میان اشیای ساده را با مدلهای سلسلهمراتبی بررسی میکند. فصل ۹ مدلسازی رویهای را معرفی میکند تا اشیا را به جای هندسهٔ صریح، با Procedure توصیف کنیم؛ این روش اجازه میدهد فقط به اندازهٔ لازم جزئیات داشته باشیم، قوانین فیزیکی را وارد مدل کنیم و پدیدههای طبیعی دشوار برای نمایش با Polygonها را مدل کنیم. فصل ۱۰ منحنیها و سطوح خمیده را اضافه میکند؛ آنها نیز با رأسها تعریف و با شکستن به Primitiveهای تخت کوچک در همان خط لولهٔ مشاهده پیادهسازی میشوند.
محور دوم واقعگرایی است. فصل ۵ تعامل نور و مواد، مدلهای Shading و حذف سطح پنهان را عمیقتر بررسی میکند و فصل ۷ روشهایی مانند Texture Mapping را برای ساخت تصاویر پیچیده از اشیای ساده معرفی میکند.
محور سوم پیادهسازی در فصل ۶ است. تا اینجا واحدهای عملکردی اصلی خط لولهٔ گرافیک معرفی شدهاند و در فصل ۶ الگوریتمهای هر واحد و امکان ساخت مستقیم تصویر در Frame Buffer بررسی میشوند.
مطالعهٔ پیشنهادی
Carlbom و Paciorek [Car78] رابطهٔ مشاهدهٔ کلاسیک و رایانهای را بررسی میکنند. Rogers و Adams [Rog90] نمونههای فراوانی از ماتریسهای projection متناظر با نماهای استاندارد نقشهکشی ارائه میدهند. Foley و همکاران [Fol90]، Watt [Wat00] و Hearn و Baker [Hea04] تبدیلهای projection کانونی را استخراج میکنند. این منابع عمدتاً جهتگیری PHIGS دارند، بنابراین API آنها اندکی با این کتاب تفاوت دارد؛ Foley عمومیترین حالت را نیز استخراج میکند. تفاوت منابع در استفاده از ماتریس سطری یا ستونی، جای COP و جهت مثبت/منفی projection روی محور (z) است. برای بحث بیشتر دربارهٔ model-view و projection در OpenGL به OpenGL Programming Guide [Shr10] مراجعه کنید.
تمرینها
4.1 همهٔ Projectionها هندسی صفحهای نیستند. مثالی بزنید که سطح تصویرسازی صفحه نباشد و مثالی که Projectorها خط نباشند.
4.2 هواپیمایی را در نظر بگیرید که موقعیت آن با Roll، Pitch، Yaw و فاصله از یک شیء مشخص میشود. ماتریس model-view را بر حسب این پارامترها بیابید.
4.3 ماهوارهای را در مدار زمین در نظر بگیرید. موقعیت آن با مختصات قطبی تعیین میشود. ماتریس model-viewای بسازید که بیننده همیشه به زمین نگاه کند.
4.4 نشان دهید چگونه جهتهای (u) و (v) را از VPN، VRP و VUP فقط با ضربهای برداری محاسبه کنیم.
4.5 آیا میتوان نمای ایزومتریک مکعب را با یک دوران منفرد حول محور مناسب به دست آورد؟ توضیح دهید.
4.6 ماتریس پرسپکتیو را هنگامی استخراج کنید که COP میتواند هر نقطهای باشد و صفحهٔ projection هر جهتگیری داشته باشد.
4.7 نشان دهید تصویرسازی پرسپکتیو خطها را حفظ میکند.
4.8 تصویرکردن نقطهای در همان صفحهٔ COP به تقسیم بر صفر منجر میشود. projection پارهخطی که دو سر آن در دو طرف صفحهٔ COP هستند چیست؟
4.9 یک یا چند API برای تعیین تصویرسازی مایل طراحی کنید؛ لازم نیست تابع را بنویسید، فقط پارامترهای لازم را مشخص کنید.
4.10 ماتریس تصویرسازی مایل را از صفحههای برش جلو/عقب و نقاط تقاطع بالا-راست و پایین-چپ اضلاع حجم برش با صفحهٔ جلویی استخراج کنید.
4.11 رویکرد نرمالسازی همهٔ projectionها ظاهراً میگوید میتوان اشیا را از قبل اعوجاج داد و فقط projection قائم پشتیبانی کرد. اگر سامانه را چنین بسازیم با چه مشکلاتی روبهرو میشویم؟
4.12 اگر COP در مبدأ نباشد، ماتریسهای projection OpenGL چگونه تغییر میکنند؟ فرض کنید COP در ((0,0,d)) و صفحهٔ projection برابر (z=0) باشد.
4.13 با Extrusion میتوان اشیای دوبعدی را به بعد سوم گسترش داد: دایره به استوانه، خط به چهارضلعی و چهارضلعی صفحهای به متوازیالسطوح تبدیل میشود. با این روش Maze دوبعدی تمرین 2.7 را به Maze سهبعدی تبدیل کنید.
4.14 برنامهٔ Maze تمرین 4.13 را طوری گسترش دهید که کاربر بتواند در آن حرکت کند. دکمهٔ میانی ماوس حرکت رو به جلو و دکمههای راست/چپ چرخش ۹۰ درجهای متناظر ایجاد کنند.
4.15 اگر محورهای مختصات را با projection قائم رسم کنیم، محورهای (x,y) در صفحهٔ کاغذ و (z) عمود بر صفحه خواهد بود. به جای آن میخواهیم (x,y) با زاویهٔ ۹۰ درجه و (z) با زاویهٔ (-135) درجه نسبت به (x) دیده شود. ماتریس projection این نما را بیابید.
4.16 برنامهای برای نمایش مکعب چرخان داخل جعبه با سه منبع نور بنویسید؛ هر نور باید مکعب را روی یکی از سه دیوارهٔ قابل مشاهدهٔ جعبه تصویر کند.
4.17 projection یک نقطه روی صفحهٔ (ax+by+cz+d=0) از منبع نوری در بینهایت با جهت ((d_x,d_y,d_z)) را بیابید.
4.18 با یکی از رابطهای سهبعدی فصل ۳ برنامهای بنویسید که دوربین را در صحنهای از اشیای ساده حرکت دهد.
4.19 برنامهای برای پرواز در واشر سهبعدی سیرپینسکی حاصل از تقسیم چهاروجهیها بنویسید. آیا میتوانید مانع عبور کاربر از دیوارها شوید؟
4.20 در پویانمایی گاهی با Patternهای دوبعدی روی Polygonهای تختی کار میکنیم که همیشه موازی دوربیناند؛ این روش Billboarding نام دارد. برنامهای بنویسید که با حرکت دوربین، یک Polygon ساده همیشه رو به دوربین بماند.
4.21 تصاویر Stereo با ساخت دو تصویر از جایگاههای کمی متفاوت بیننده تولید میشوند. بینندهای در مبدأ با فاصلهٔ دو چشم (x) واحد را فرض کنید. مشخصات مشاهدهٔ مناسب برای دو تصویر چیست؟
4.22 در بخش 4.9 Mesh را با دو Line Strip رسم کردیم. روش را طوری تغییر دهید که خط اضافی از انتهای یک سطر/ستون به ابتدای بعدی رسم نشود.
4.23 روشی برای نمایش Mesh با یک Triangle Strip منفرد استخراج کنید.
4.24 Fragment Shaderای بسازید که در projection پرسپکتیو Polygon Offset انجام دهد.
4.25 Shaderای بنویسید که ارتفاع Mesh را در خود Shader تغییر دهد.
4.26 یک Mesh مستطیلی را به صورت یک Triangle Strip واحد رندر کنید؛ در انتهای هر سطر یک مثلث Degenerate ایجاد کنید.
4.27 برنامهای برای پرواز بر فراز Mesh بنویسید. کاربر باید بتواند به اطراف، تپهها و درهها نگاه کند و مجبور نباشد همیشه به یک نقطهٔ ثابت نگاه کند.
4.28 یک Reshape Callback بنویسید که با تغییر اندازهٔ پنجره، شکل اشیا دچار اعوجاج نشود.