גרפיקה ממוחשבת - הספר
ארבעה פרקים ראשונים
אשמח לקבל חוות דעת
התקנת סביבת העבודה
שני שלבים:
התקנת visual c++
התקנת glut
שלב 1 : התקנת visual c++
1. הרץ ב-goggle את המחרוזת ""Microsoft visual studio c++ 2008 express
2. בחר בשפה 'אנגלית' והורד את הקובץ vcsetup אל שולחן העבודה שלך.
3. הרץ את הקובץ ובצע את ההנחיות. יתכן ותחויב בהרשמה לאחד המועדונים המפוארים של היצרן (אין ארוחות חינם...) אם ברשותך נתוני חיבור של messenger
רשום אותם. יתכן ותצטרך לחדש הרשמה זו מדי פעם.
4 . בסיום התהליך התווספו למחשבך כמה סביבות עבודה חדשות והחשובה שבהן היא visual c++ .הרץ מרשימת התוכניות את Microsoft Visual C++ 2008 Express Edition
וודא תקינות בהפעלה.
5 . סגור את התוכנית.
הערה: נכון לעכשיו קיימת גרסה 2010 של כל סביבת Express החינמית. יתכנו בה שינויים כמו intellisense יותר ידידותי או התקנת glut שם ולא כאן. מלבד זאת אין הבדל. הכרת גרסה אחת אתה שולט בשאר. מה עוד שגרסת 2008 מוכחת.
שלב 2: התקנת glut
עבור לקישור הבא: http://www.xmission.com/~nate/glut.html
הורד את הקובץ glut–3.76-bin.zip
פרוש את הקובץ לתיקיה בשם זה. בתיקיה נוצרו שלושה קבצים:
glut32.dll glut32.lib glut.h
ההוראות שלהלן הן פקודות DOS הישן שלפעמים מאד שימושי:
copy glut32.dll "d:\windows\system"
copy glut32.lib "d:\Program Files\Microsoft SDKs\Windows\v6.1\Lib"
copy glut.h "d:\Program Files\Microsoft SDKs\Windows\v6.1\Include\gl"
ההוראות מעתיקות את קבצי התמיכה של OpenGL אל התיקיות של windows ואל SDK.
תחביר הפקודה פשוט: העתק מספריה(במקרה זה מספרית ברירת המחדל בה מצוי הקובץ)שם קובץ אל ספרית היעד.
ניתן לבצע העתקה ידנית (copy/paste ) של כל קובץ. או להריץ כל הוראה משורת DOS. אפשר כמוני, לכתוב את שלוש השורות בקובץ טקסט (למשל בעורך notepad או בעורך c++ רק לא Word או מעבד תמלילים כלשהוא) להעניק לקובץ שם עם סיומת BAT למשל:install. bat ולהריץ את הקובץ. הקבצים יועתקו מאליהם.
בניית פרויקט חדש
בהנחה שעברנו את שלבי ההתקנה בהצלחה, נפתח פרויקט חדש.
חפש את הצלם של visual c++ בתפריט תוכניות והקש עליו.
בתפריט בחר: file->new->project
יפתח המסך:
בחר בחלון השמאלי Win32 ובימני Win32 Console Application
בחר שם לפרויקט (name ) בחר מיקום (location ) ודא שהתיבה Create directory for solution מסומנת. הקש OK .
התקבל המסך:
הקש Next
בחר Empty project והקש Finish
וזו התוצאה:
מעט על התפריט:
ביצוע "קליק" ימני על שם הפרויקט בחלון השמאלי מאפשר הוספה (Add ) של פריט הקיים בספריה אך עדיין לא בפרויקט, הוספה של פריט חדש והחשוב מכל, הוספת : Class,מחלקה.
נדגים זאת פעם אחת. ניצור ונוסיף את המחלקה cPoint לפרויקט. ניתן להגיע לאותה בחירה דרך:
Project -> Add Class.
נבחר Add
בחר Finish.התקבל:
הסבר:
לפרויקט נוספו שני קבצים:
cPoint.h זהו קובץ הכותרת כמקובל בשפות c/c++ המכיל הגדרות של פונקציות תכונות/ משתנים ועוד.
cPoint.cpp וזה קובץ המימוש.
ישאל הקורא מדוע cPoint ולא CPoint ?
הרי נאותות הכתיבה דורשת:
1) משתנים : באותיות קטנות
2) פונקציות / פעולות מחלקות : אות ראשונה גדולה. ואם המזהה מורכב ממספר מילים אזי, כל אות ראשונה במילה – גדולה.
3) בשפת c/c++ שם המחלקה תמיד יתחיל באות גדולה : C
כך ננהג בהמשך .אך במקרה של מחלקת הנקודה CPoint היא מילה שמורה בסביבת visual c++ .מאחר והשפה היא Case Sensitive האותיות c , C יוצרות מזהים שונים זה מזה.
ובכן, שני קבצים מרכיבים את המחלקה בשפת c++ (במקור בשפת c ):*.h ,*.cpp
כשהתוכנית רצה נקראות פונקציות מקבצי המימוש השונים- *.cpp.המהדר ("קומפיילר")
לא מכיר את הגדרת הפונקציה ולכן מבצעים "הכללה" (include ) לקובץ הכותרת - *.h
מבנה קובץ הכותרת:
#pragma once
שורה זו נכתבת אוטומטית בעת יצירת המחלקה ומטרתה למנוע "הכלה מרובה" של קובץ כותרת זה.
class cPoint
{
הגדרת משתנים/ פונקציות/ תכונות
};
ההגדרות השונות יופיעו בין הצומדיים }{.
חשוב:
התו ; (נקודה פסיק) חייב להופיע לאחר הסוגר הסוגר. חסרונו עלול לדרוש הרבה זמן debug .מניסיון!
מבנה קובץ המימוש:
#include "cPoint.h"
cPoint::cPoint(void)
{
}
cPoint::~cPoint(void)
{
}
בשורה הראשונה הוראת המקרו #include המבצעת הכלה של קובץ ההגדרות.
בהמשך שתי פונקציות שנבנו ע"י הסביבה: הראשון constructor ("בנאי", "פעולה בונה")
והשני destructor פעולה המשמשת להריסת אובייקט.
בראשונה נבצע שימוש נרחב ,בשנייה פחות.
מעט על constructor:
פונקציה חברה במחלקה למעט :היא נקראת פעם אחת בזמן יצירת עצם("אובייקט").אין אפשרות לזמן constructor כמו כל פונקציה, אפשר לשלוח/להעביר פרמטרים אך אין ערך מוחזר. אם נוסיף ל constructor שלעיל את ההוראות:
cPoint::cPoint(void)
{
x = y = z = 0;//כתיבה מקוצרת בשפה
}
אזי ברגע שנבנה עצם מהמחלקה cPoint בעזרת ההוראה : cPoint p;
נקרא ה constructorבאופן אוטומטי ונוצר "מופע" ("instance ") חדש של עצם בשם p כלומר נקודה ששיעוריה (קואורדינאטות) הן: p.x=p.y=p.z=0 ובמילים פשוטות: נקודה חדשה בשם p בראשית הצירים.
מעט על destructor:
משמש להריסת העצם בעקבות קריאה יזומה, ולשחרור הזיכרון ששימש עצם זה.
ועוד משהוא כללי:
נעשה כאן שימוש באופרטור :: ,אופרטור הטווח, כאשר: cPoint הראשון הוא שם המחלקה והשני הוא שם פונקציה חברה (member function) בטווח, או במקרה זה במחלקה.
התחביר המלא הוא:
Return_value Class_name::Function_name(Parametrs_list);
אז זהו חברים. נתחיל לעבוד
פרק 1: הנקודה, Point
המרכיב הקטן ביותר של כל אובייקט גרפי במישור ובמרחב היא הנקודה. מלבד הממדים של הנקודה הנקבעים ע"י סביבת העבודה אנו מאפיינים את הנקודה בעזרת שניים:
1 . שיעורי הנקודה (במישור או במרחב)
2 . צבע הנקודה (בשיטת R.G.B)
//file:cPoint.h
#pragma once
class cPoint
{
public:
cPoint(void);
~cPoint(void);
// member functions, set/get properties
private:
double x,y,z; // coordinates in 3d plane
double r,g,b; // point's color
.
.
.
};
שיעורי הנקודה הם ערכים ממשיים (double) כאשר:
- בעבודה בדו מימד עלינו למפות את העולם הממשי בהתאם לרזולוציית המסך.
- בעבודה בתלת מימד עלינו למפות את העולם הממשי לפי ערכי glut .הסבר בהמשך.
צבע הנקודה נקבע ע"י מספר ממשי בתחום 0..1 כאשר:
- שלושת מרכיבי הצבע הם r-Red, g-Green, b-Blue שערכיהם בתחום האמור.
- צבע הנקודה נקבע לפי שלושת המרכיבים: color = (red ,green ,blue)
גישה (qualifiers)"לחברי המחלקה" protected,public ,private :
הסתרה או הכמסה (מלשון כמוס – סוד) של נתונים היא אבן יסוד בתכנות מונחה עצמים.ולכן פונקציות של המחלקה שמיועדות להיקרא ממחלקה אחרת מופיעות בקטע public ואז ניתן לזמן אותן מחוץ למחלקתם ,
אך משתנים פרטיים של המחלקה מופיעים בקטע private כדי שלא ישונו מחוץ למחלקה.
איך בכל זאת ניתן לקבוע ערך למשתנה פרטי או לקבל את ערכו? בעזרת פונקציות Set/Get
שמיד נכתוב.
לעיתים נשתמש במאפיין הגישה protected שמשמעו :public לחברי המחלקה הנורשת אך private לשאר.
איתחול המשתנים הפרטיים:
ניתן לביצוע בשתי דרכים: constructor,פונקציה Set().
- איתחול בעזרת constructor:
נוסיף לקובץ cPoint.h את ההגדרה:
cPoint(double x, double y, double z);
נוסיף לקובץ cPoint.cpp את המימוש:
cPoint::cPoint(double x, double y, double z)
{
this->x=x;
this->y=y;
this->z=z;
}
אם נרשום בקובץ "היורש" מהמחלקה cPoint או המכיל עצם מהמחלקה את ההוראות:
cPoint stam(1,2,3);
אזי בעקבות ביצוע ההוראה נוצרה נקודה בשם stam ששיעוריה הם 1,2,3 על הצירים x,y,z בהתאמה.
ועוד הסבר קטן: הכתיבה
this->x=x;
באה להבחין בין המשתנה הפרטי x לבין הפרמטר באותו השם תוך שימוש ב"מצביע" this.
המצביע this הוא למעשה מצביע למופע של אובייקט מהמחלקה. במקרה זה מצביע על הנקודה stam .
אפשרית גם כתיבה בצורה x=x אלא שהיא כתיבה לא ברורה ולא נאותה.
כמובן גם שאפשר להעניק לפרמטרים מזהים אחרים כמו:xcoordinate וכתוב:
x = xcoordinate;
הבחירה בידכם.
- איתחול בעזרת פונקציה Set();:
נוסיף לקובץ cPoint.h את ההגדרה:
void SetPointCoordinates(double x, double y, double z);
נוסיף לקובץ cPoint.cpp את המימוש:
void cPoint::SetPointCoordinates(double x, double y, double z)
{
this->x=x;
this->y=y;
this->z=z;
}
בעקבות צמד ההוראות :
cPoint stam;
stam. SetPointCoordinates(1,2,3);
נוצרה נקודה בשם stam ששיעוריה הם 1,2,3 על הצירים x,y,z בהתאמה.
כתבנו וממשנו פונקציה בשפה,אז:
void ערך מוחזר של הפונקציה.במקרה זה , אין ערך מוחזר.לחילופין,ערך מוחזר ריק
cPoint שם המחלקה.
:: אופרטור הטווח.
SetPointCoordinates שם המחלקה ולאחריה רשימת פרמטרים.
התחלנו עם פונקציה מהמשפחה Set(),להשמת ערכים נמשיך עם פונקציה ממשפחת Get() המחזירה ערך מטיפוס double:
נוסיף לקובץ cPoint.h את ההגדרה:
double GetX(void);
נוסיף לקובץ cPoint.cpp את המימוש:
double cPoint::GetX(void)
{
return this->x;
}
אם נרשום את ההוראה : double xcoordinate = stam.GetX();
אזי המשתנה xcoordinate=1.
הערה:
במקרה של פונקציה קצרה ניתן ומקובל לקצר את הכתיבה כך שקובץ הכותרת של המחלקה יכיל הן את ההגדרה והן את מימוש הפונקציה. למשל כך:
//file : cPoint.h
double GetX(void){ return this->x; };
עכשיו הוסיפו בעצמכם פונקציות לטיפול בצבע של הנקודה.לא נוכל לבצע זאת עם עוד constructor כמו זה :
cPoint(double r, double g, double b);
זוהי כפילות.
אם רוצים להוסיף constructor או פונקציות בעלות אותו שם ניתן לעשות זאת בתהליך הנקרא העמסה על פי כלל פשוט :
constructor או פונקציה מועמסים יהיו שונים במספר או בטיפוס הפרמטרים שלהם.
במקרה זה ניתן לרשום constructor כך:
cPoint(double x, double y, double z, double r, double g, double b);
ולממש בהתאם ,אלא שזו כתיבה מייגעת ומקור לטעויות. נסתפק אם כך בפונקציה:
void SetPointColor(double r, double g, double b);
ונממשה בהתאם. נוסיף ונממש את הפונקציות:
double GetY(void);
double GetZ(void);
double GetR(void);
double GetG(void);
double GetB(void);
בסך הכול נקודה אחת קטנה וכל כך הרבה עבודה.יש עוד משהוא ? כן והרבה.
תחשבו ,אפשר להזיז נקודה ואפשר לסובב נקודה סביב רדיוס כלשהוא ואפשר להגדיל או להקטין את המרחק בין שתי נקודות.נניח שציירנו על המסך קובייה.עכשיו נרצה :
להזיז את הקובייה לכוון כלשהוא.כלומר להזיז את כל הנקודות השייכות לקובייה לאותו כיוון ובאותו שיעור.
לסובב את הקובייה סביב מרכזה, כלומר לסובב כל נקודה המרכיבה את הקובייה סביב מרכזה.
נרצה ל"נפח" או לכווץ את הקובייה , כלומר , להגדיל או להקטין את המרחק בין כל שתי נקודות סמוכות של הקובייה, באותו ערך כמובן.
פעולות אלה, המשנות את מיקומו של האובייקט במסך ,נקראות טרנספורמציה.
טרנספורמציה היא תורה שלמה ,המערבת טריגו ופעולות על מטריצות. מאחר וטרנספורמציה על אובייקט פירושה טרנספורמציה של כל נקודה באובייקט נדון כאן בהזזה של נקודה. משיקולים של יעילות הקוד , לא נערב מטריצות בשלב זה.
הערה - מטריצות : מערך דו ממדי ,מבחינתנו ריבועי. כפל המוגדר באלגברה על שתי מטריצות הוא מסדר גודל O(n3) והקוד עצמו בכלל לא פשוט. נעשה זאת בהמשך.
טרנספורמציה על נקודה:
הזזה : Translate() הזזת קואורדינטה אחת או יותר של הנקודה:
void cPoint::TranslateX(double deltaX)
{
this->x += deltaX;//x = x + deltaX כתיבה מקוצרת בשפה שפירושה:
}
בעקבות ההוראה stam.TranslateX(value) תוזז הנקודה stam למיקום חדש : x+deltaX
אם value > 0 ההזזה ימינה אחרת value < 0 , שמאלה.המיקום על y,z ללא שינוי. אם נזיז את כל נקודות האובייקט בשיעור value הרי שהאובייקט כולו יוזז בשיעור זה למיקום חדש.
נגדיר ונממש את הפעולות:
void TranslateY(double deltaY);
void TranslateZ(double deltaZ);
סילום : Scale() שינוי קנה מידה קואורדינטה אחת או יותר של הנקודה:
void cPoint::ScaleX(double factor)
{
this->x *= factor;
}
בעקבות ההוראה stam.ScaleX(value) ישתנה המיקום של הנקודה יחסית לראשית הצירים. אם נפעל כך על כל נקודות האובייקט אזי, האובייקט יקטן/יגדל וגם יזוז למיקום חדש. כל זאת, אם : value > 1 האובייקט "יתנפח" אחרת value < 1 > 0 האובייקט "יתכווץ". בשני המקרים האובייקט יוזז. פעולת סילום חשובה נוספת היא ScaleSelf() המבצעת סילום יחסית למרכז הגוף(ולא לראשית הצירים) נדון בה בהמשך.
נגדיר ונממש את הפעולות:
void ScaleY(double factor);
void ScaleZ(double factor);
עכשיו, עשו לעצמכם טובה , קחו דף ועיפרון וציירו משולש (מצולע, פוליגון) העובר בנקודות p1,p2,p3
- ובצעו הזזה על כל הנקודות עבור ערכי
deltaX >0, DeltaX <0, deltaX = 0 . ציירו את המשולש במיקום החדש.
- בצעו סילום (מספיק על X ) על שלוש הנקודות. קיבעו את ערכי factor כך שהמשולש:
ü יישאר במקום.
ü "יתכווץ".
ü "יתנפח".
אם זה עבד על הדף , הכול בסדר. זה יעבוד גם בקוד התוכנית.
סיבוב : Rotate() : סיבוב הנקודה יחסית לנקודת "ציר"(pivot point):
פעולה זו , מורכבת מקודמותיה , משמשת לסיבוב האובייקט סביב נקודה כלשהיא ("שבת" בסגול) בדרך כלל סביב מרכז האובייקט. להבנת הפעולה נדון בסיבוב נקודה אחת בזווית כלשהיא alpha יחסית לראשית הצירים.
חשוב לקרוא ולהבין ... נא להתנהג בהתאם! (או להתאזר בסבלנות , וכולי וכולי...)
נתונה נקודה p(x1,y1) בזווית a כלפי ציר x . נסובב את הנקודה בתוספת זווית b נגד כיוון השעון (כיוון מתמטי חיובי).
קיימים בבירור הקשרים עבור הנקודה במיקום (x,y):
x = r cos(a);
y = r sin(a);
המיקום החדש של הנקודה (x1,y1) ניתן להצגה:
x1 = r cos(a+b) = r cos(a)cos(b) – r sin(a)sin(b)
y1 = r sin(a+b ) = r sin(a)cos(b) + r cos(a)sin(b)
נציב את הערכים משתי המשוואות הראשונות ונקבל:
x1 = x cos(b) – y sin(b)
y1 = y cos(b) – x sin(b)
נשארה עוד בעיה אחת קטנה: לתרגם זאת לקוד. אז הנה:
void cPoint::RotateZ(double alpha)
{
double x1=x,y1=y;
x = x1 * cos(alpha) - y1 * sin(alpha);
y = y1 * cos(alpha) + x1 * sin(alpha);
}
זווית הסיבוב היא alpha , משתנים מקומיים x1,y1 מקבלים את ערכי משתני המחלקה x,y כלומר המיקום הנוכחי של הנקודה, ותוצאת הנוסחאות מבצעת השמה לתוך משתני המחלקה x,y כלומר מיקומה החדש של הנקודה בקואורדינטות אלה.
עכשיו תשאל : למה RotateZ , ואיפה בכלל ציר z ?
סובבנו את הנקודה סביב ציר z (זה הציר הניצב למסך) ולכן השינוי הוא רק בשתי הקואורדינטות האחרות x,y .
הנה ההסבר:
ביישומים גרפיים אנו מעתיקים את "העולם הממשי" לתוך מסך המחשב ,כלומר מערכת הצירים (הקרטזית) מועתקת לתוך המסך , אך בשינוי קל :
על הדף:
y
x
z
במסך:
+x
(0,0,0)
z
+y
במספר מערכות גרפיות בהן windows זה המקובל. במערכת glut , שונה מעט. נסביר בהמשך. כל שנותר הוא להגדיר ולממש שתי פונקציות נוספות: סיבוב הנקודה סביב הצירים x,y .
void RotateX(double alpha);
void RotateY(double alpha);
עשו זאת.
גם כאן קצת חומר למחשבה: לאיזה כיוון תסתובב הנקודה עבור ערכי alpha שליליים ,חיוביים ? ברור שלכיוונים שונים.
לסיכום המחלקה כולה:
#pragma once
class cPoint
{
private:
double x, y, z, r,g,b;
public:
cPoint(void);
virtual ~cPoint(void);
cPoint(double x, double y, double z);
void SetPointCoordinates(double x, double y, double z);
void SetPointColor(double r, double g, double b);
void TranslateX(double deltaX);
void TranslateY(double deltaY);
void TranslateZ(double deltaZ);
double GetX(void);
double GetY(void);
double GetZ(void);
double GetR(void);
double GetG(void);
double GetB(void);
void SetX(double x);
void SetY(double y);
void SetZ(double z);
void SetR(double r);
void SetG(double g);
void SetB(double b);
void ScaleX(double factor);
void ScaleY(double factor);
void ScaleZ(double factor);
void RotateX(double alpha);
void RotateY(double alpha);
void RotateZ(double alpha);
};
ממשו הגדרות אלה.
בחלון solution explorer סמנו בקליק ימני את הקובץ cPoint.cpp ובחלון הנפתח בחרו compile .תקנו את הדורש תיקון עד לקבלת
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
בחלון output שבתחתית המסך.
הבחנתם בוודאי שהפעם התמונה היא של גרסה visual 2010 .אין הבדל מבחינתנו.
פרק 2 :המשטח , Polygon
אם שתי נקודות יכולות להגדיר קו אזי שלוש נקודות ויותר יכולות להגדיר משטח. בהמשך ניצור מערך שבו כל איבר הוא נקודה וכל האיברים מגדירים משטח וכל שנותר, הוא לצייר משטח זה בצמוד למשטחים נוספים ולקבל צורה בתלת ממד כמו קובייה, פירמידה ,כדור ועוד.
הפעם העבודה תהיה פשוטה יותר מאשר במחלקה cPoint .נעתיק את רוב הפונקציות ממחלקת נקודה למחלקה החדשה CPolygon בשינויים קלים ובפרט : נבצע את הפעולה על כל אחת מנקודות המשטח. לא מסובך כלל, לשם כך יש לולאות ומערכים.
ראשית נבנה מחלקה חדשה בשם CPolygon כשהתוצאה היא שני קבצים חדשים:
Polygon.cpp,Polygon.h .
1. נגדיר את מערך נקודות points[]
2. נגדיר את פונקציות הטרנספורמציה, על מערך הנקודות
3. נגדיר את פונקציית הציור על המסך.(סבלנות...עדין אין דבר על המסך)
וזה הולך כך:
נמשיך ונסביר:
המצביע *points הוא משתנה מהמחלקה cPoint המוכל (יחס "הכלה") במחלקה CPolygon ולכן ניתן להתייחס דרכו ל"חבריו" שבמחלקה cPoint מתוך מחלקת CPolygon ."חבריו" הן הפונקציות השונות במחלקה cPoint המוגדרות במאפיין public .לא ניתן להתייחס למשתני private (x,y,z,r,g,b ) דרכו. המצביע * מגדיר כתובת בזיכרון של המשתנה points ובכתובת זו רשום ערכו של המשתנה. מאחר ואנו מגדירים מערך בעזרת המצביע , הכתובת הנ"ל, היא כתובתו של איבר מספר 0 במערך.
בעזרת מצביע ניתן לבצע הקצאת זיכרון (דינמית) דרך הפנקציה SetNumberOfPoints :
void CPolygon::SetNumberOfPoints(int numofpoints)
{
this-> numofpoints = numofpoints;
points = new cPoint[numofpoints];
}
האופרטור new מבצע הקצאה ל numofpoints איברים ,כל אחד מהם מהטיפוס (מחלקה) cPoint .הגודל בבתים נקבע במקרה זה ע"י המהדר.(הקצאה בעזרת new ניתנת להסרה בעזרת האופרטור delete . מקובל דרך ה destructor)
נדגים שימוש במערך בעזרת הפונקציה: SetPointCoordinates :
void CPolygon::SetPointCoordinates(int i, double x, double y, double z)
{
points[i].SetPointCoordinates(x,y,z);
}
הפרמטר i חייב לקבל ערכים בין 0..numofpoints-1 שהם האינדקסים של איברי המערך. אחרת שגיאת ריצה (אפילו לא שגיאת קומפילציה). מכאן points[i](שהיא אחת הנקודות של המשטח) הוא אובייקט של המחלקה cPoint ובעזרת האופרטור "." ניתן לגשת לכל חבר public במחלקה cPoint . אחת מהן היא הפונקציה
void cPoint::SetPointCoordinates(double x, double y, double z)
{
this->x=x;
this->y=y;
this->z=z;
}
המקבלת את הפרמטרים של הקואורדינטות ומשימה אותם לתוך משתני המחלקה של נקודה זו שמספרה הסידורי במחלקה שמעל הוא i .
עצרו רגע. זהו תכנות מונחה עצמים. שאלו מה קורה כשיש הרבה נקודות? איך אין בלבול...
ובכן לכל נקודה במערך המוגדר במחלקה CPolygon שמור עותק ובו הערכים.
נדגים עוד שימוש במערך points בעזרת הפונקציה: void TranslateX(double deltaX);
void CPolygon::TranslateX(double deltaX)
{
for(int i=0;i<numofpoints;i++)
points[i].TranslateX(deltaX);
}
אנו סורקים את כל איברי המערך points .כל איבר הוא "אובייקט" של מחלקת cPoint או מופע של המחלקה ובפשטות נקודה אחת של המשטח.
עבור כל נקודה במערך אנו מפעילים את הפונקציה החברה במחלקה cPoint ושמים בתוכה את הערך החדש של הקואורדינטה x כך ש: x=x+deltaX.
כל המשטח יזוז בערך deltaX על ציר ה x . אם deltaX < 0 יזוז שמאלה.ואם deltaX > 0 יזוז המשטח ימינה. אחרת יישאר במקומו.
המשיכו וממשו את שאר הפונקציות, המבצעות טרנספורמציה. קמפלו ושימרו את הקובץ לאחר כל שינוי.
הפונקציות GetCEnter מחשבות את נקודת המרכז של כל משטח ובהמשך של האובייקט כולו, לדוגמא:
double CPolygon::GetCenterX()
{
double sum = 0.0;
for(int i=0;i<numofpoints;i++)
sum+=points[i].GetX();
return sum/numofpoints;
}
המשתנה sum סוכם את ערכי הקואורדינטות x של כל נקודות המשטח. הפונקציה מחזירה את הממוצע של הסכום כלומר מרכז המשטח על ציר x .פונקציות אלה שימושיות כשאנו רוצים לסובב את הגוף סביב מרכזו או לבצע עליו zoom מבלי להזיזו מהמקום. נוכל לבצע סיבוב או zoom של כל האובייקט יחסית לאחד או יותר משלוש הצירים.
ממשו את שתי הפונקציות הנותרות.
לפניכם תחיל הקובץ polygon.cpp הקפידו על הכתוב:
#include "Polygon.h"
#include <GL/glut.h>
CPolygon::CPolygon(void)
{
numofpoints=0;
}
CPolygon::~CPolygon(void)
{
}
ביצענו שתי הכללות: האחת כמובן לקובץ Polygon.h כמקובל והשנייה לקובץ glut.h
ההכללה הראשונה #include "Polygon.h" פירושה שהקובץ המוכלל מצוי בספריית הפרויקט.
ההכללה השנייה #include <GL/glut.h> פירושה שהקובץ glut.h מצוי בספריית SDK לשם אנו העתקנו אותו בשלב התקנת הסביבה. הקפידו על סוגרים מחודדים או על מרכאות בהוראות השונות. יחסוך ממכם זמן debug ארוך.
ועכשיו רבותי סיבה למסיבה: הגענו לפונקציית הציור DrawPoly() .
בכיתה לוקח הרבה זמן ומאמץ להגיע לפה. איך אומרים ...עשיתם כברת דרך.
void CPolygon::DrawPoly()
{
glBegin(GL_POLYGON);
for(int i=0;i<numofpoints;i++)
{
glColor3f( points[i].GetR(),points[i].GetG(),points[i].GetB());
glVertex3f(points[i].GetX(),points[i].GetY(),points[i].GetZ());
}
glEnd();
}
ההוראות glBegin , GL_POLYGON , glColor3f , glVertex3f , glEnd הן כולן הגדרות של glut.h ולכן גם הכללנו קובץ זה.
הפונקציה סורקת את איברי המערך-הנקודות, ולכל נקודה שולפת שני ערכים המאוחסנים במערך: ערכי הצבע וערכי המיקום על שלוש הצירים.
ערכי הצבע: glColor3f – צבע של שלושה ערכים לפי שיטת r,g,b שנתונים כממשיים (float, double ).
ערכי המיקום : vertex באנגלית – קדקוד.vertices ברבים. ולכן glVertex3f מכיל שלושה ערכי double או float של x,y,z ,שלושת הצירים בהתאמה.
בין שתי ההוראות glBegin() , glEnd() יכולות להופיע רק פקודות אלה.(הנחיות מדויקות תמצאו בספרים:Redbook , bluebook של OpenGL המופיעים ברשת.
ההנחיה glBegin(GL_POLYGON) מחברת את הנקודות הסמוכות במערך ומציגה אותן בצבע הנתון. אם לא קבענו צבע לנקודה יבחר צבע default .עד כאן רבותי בפרק זה. אתם ,מן הסתם, רוצים לצייר משהוא על המסך ולבצע עליו מניפולציות. אך זה יהיה בדו ממד ויתאים לדרישות של 3 יח"ל .אז עוד קצת סבלנות (קטונתי מלומר זאת) ונבנה אובייקטים מרשימים.
ושוב קמפלו ושימרו .דבגו ותקנו ושוב שימרו. אל תבצעו run כי אין עדיין את הפונקציה main() בלעדיה היישום הוא *.dll ולא *.exe הנדרש להרצה.
ועוד משהוא: חצי התנצלות, הדבקתי תמונה ולא קובץ עליו ניתן לבצע copy , paste כי חייבים לתרגל.
פרק 3 :האובייקט בתלת מימד, Model
התחלנו מנקודה במרחב, עברנו לאוסף של נקודות : שתי נקודות מגדירות קו, שלוש ויותר מגדירות משטח , במישור ובמרחב התלת ממדי.
אוסף של משטחים יכול להגדיר צורה, אובייקט בתלת ממד. וזאת נעשה. נגדיר מערך חדש בשם *polygon במחלקה CModel ,כל איבר במערך הוא משטח – פוליגון .אם "נחבר" את כל "הפוליגונים " כהלכה נקבל צורה(אובייקט, מודל) בתלת ממד, לדוגמא: קובייה, פירמידה, כדור. איך נחבר את " כל 'הפוליגונים' כהלכה" ? בזמן בניית המודל אנו קובעים את הקואורדינטות של כל נקודה. ונדאג ,למשל בקובייה, שכל קדקוד יהיה משותף לשלושה משטחים.
לפני שנתחיל-נמשיך, בואו נחשוב על ההירארכיה שבפרויקט. לפניכם המערך polygon המכיל 6 איברים (שש פאות הקובייה, למשל)
5 4 3 2 1 0
Points[]
3 2 1 0
הנקודה : ),(r,g,b)} cPoint::{( x,y,z
כלומר: מערך של 6 פוליגונים, כל פוליגון הוא מערך של 4 נקודות (points). בסה"כ 24 נקודות. איך? בקובייה רק 8 קדקודים . ובכן חלקו ב 3 כי כל קדקוד משותף ל 3 פאות. האם יש פה כפילות בנתונים? כן! זה "המחיר" שאנו משלמים כדי שנוכל לשלוט בכל נקודה ובכל פוליגון ובמודל כולו. מניסיון "הבזבוז" הזה שווה את המחיר. מה עוד שהאובייקט מורכב ממספר "קטן" של נתונים, וזה גם יאפשר לצבוע כל פאה בצבע משלה.
כשבונים כדור ממאות ואולי 1000 נקודות ,אז יש להתחשב בכך משיקולים של יעילות.
כל מי שלמד/לומד עיצוב תוכנה(מבני נתונים ואלגוריתמים) זה הזמן לחשוב על יעילות האלגוריתם הן בזיכרון המחשב והן במניפולציות על המודל. אז ,
i. נבנה את המחלקה CModel
ii. נגדיר בעזרת מצביע מערך של פוליגונים : polygon[]
iii. נעתיק בשינויים קלים את פונקציות הטרנספורמציה ואת פונקציית הציור.
#pragma once
#include <math.h>
#include "Polygon.h"
#ifndef Pi
#define Pi 3.14159265358979323846
#endif
class CModel
{
protected://can be private
int numofpolygons;// מספר הפוליגונים של האובייקט
CPolygon *polygon;//מערך הפוליגונים
public:
CModel(void);
virtual ~CModel(void);
void SetNumOfPolygons(int numofpolygons);
int GetNumOfPolygons() {return numofpolygons;}
double GetX(int i,int j){return polygon[i].GetX(j);}//i=polygon's number
double GetY(int i,int j) {return polygon[i].GetY(j);}//j=point's number
double GetZ(int i,int j) {return polygon[i].GetZ(j);}
void SetX(int i,int j,double x) {polygon[i].SetX(j,x);}
void SetY(int i,int j,double y) {polygon[i].SetY(j,y);}
void SetZ(int i,int j,double z) {polygon[i].SetZ(j,z);}
void TranslateX(double deltaX);
void TranslateY(double deltaY);
void TranslateZ(double deltaZ);
void ScaleX(double factor);
void ScaleY(double factor);
void ScaleZ(double factor);
void ScaleSelfX(double factor);
void ScaleSelfY(double factor);
void ScaleSelfZ(double factor);
void RotateX(double alpha);
void RotateY(double alpha);
void RotateZ(double alpha);
void RotateSelfX(double alpha);
void RotateSelfY(double alpha);
void RotateSelfZ(double alpha);
double GetCenterX();
double GetCenterY();
double GetCenterZ();
void DrawPoly();
};
הסברים :
המקרו
#ifndef Pi
#define Pi 3.14159265358979323846
#endif
מגדיר את הקבוע ההנדסי "פיי" Л אם הוא לא מוגדר בקובץ math.h אותו הכללנו. ההוראה בשפות c/c++ #define Pi 3.14159265358979323846 מגדירה קבוע בשם Pi שזה ערכו כלומר: Pi = 3.14159265358979323846.
מכאן העבודה הופכת מעט שגרתית (נא לא להשתעמם...).הפונקציות כמעט ללא שינוי:
שם עבדנו על מערך של m נקודות וכאן נעבוד על מערך של n פוליגונים.
include "Model.h"
CModel::CModel(void)
{
numofpolygons=0;
}
CModel::~CModel(void)
{
}
void CModel::SetNumOfPolygons(int numofpolygons)
{
this->numofpolygons = numofpolygons;
polygon = new CPolygon[numofpolygons];
}
void CModel::TranslateX(double delatx)
{
for(int i=0;i<numofpolygons;i++)
polygon[i].TranslateX(delatx);
}
קראו היטב: הפונקציה TranslateX(double delatx) מזיזה את כל האובייקט בשיעור deltaX (לימין או לשמאל) על ציר ה- X כמובן. מכאן הפונקציה מזיזה כל פוליגון של האובייקט , כלומר את כל נקודותיו.עיקבו אחר התהליך מתוך סביבת הפיתוח: קליק ימני על TranslateX(delatx) ובחרו Go To Definition .איזה פלא :הגעתם לאותה הפונקציה אך בקובץ Polygon.cpp .בצעו שם אותו קליק ימני והגעתם לקובץ cPoint.cpp .טיילתם במורד ההירארכיה.
נכון ,יש פה עבודה "מיותרת". הזזנו קדקוד אחד 3 פעמים. אתם מוזמנים לייעל את האלגוריתם.
ממשו את פונקציות set,get הנותרות. נתעכב על הפונקציות GetCenter ,בהן נעזרות הפונקציות RotateSelf, ScaleSelf המבצעות סיבוב וניפוח/כיווץ סביב מרכז האובייקט.
ובכן :
double CModel::GetCenterX()
{
double sum = 0.0;
for(int i=0;i<numofpolygons;i++)
sum+=polygon[i].GetCenterX();
return sum/numofpolygons;
}
לכל פוליגון מוצאים את מרכזו , וסוכמים ערך זה לתוך sum .הערך המוחזר הוא ממוצע המרכזים של כל הפוליגונים. למעשה מוחזרת "נקודת שיווי המשקל" של האובייקט בציר כלשהו.
סביב נקודה זו אפשר לבצע Scale לכל האובייקט ואפשר לסובבו סביב צירו. כל זאת מבלי שהאובייקט יזוז ממרכזו. כדי לבצע אחת מפעולות אלה, חובה להביא את מרכז האובייקט לראשית הצירים. ואז אפשר לבצע Zoom או Rotate ולבסוף להחזירו למקומו המקורי. רק בסיום העבודה שהתבצעה כולה בזיכרון, מציירים שוב את האובייקט על המסך, כלומר ניתן להבחין רק שהגוף סובב או הוגדל .לא ניתן להבחין בתהליך. הנה עוד משהו שאתם המפתחים יודעים ומבינים. המשתמש, לא.
void CModel::ScaleSelfX(double factor)
{
double centerx = GetCenterX();
double centery = GetCenterY();
double centerz = GetCenterZ();
TranslateX(-centerx);
TranslateY(-centery);
TranslateZ(-centerz);
ScaleX(factor);
TranslateX(centerx);
TranslateY(centery);
TranslateZ(centerz);
}
כל הזימונים שלעיל הן לפונקציות חברות במחלקה (CModel) ולכן ניתן "לקרוא" להן ללא שם עצם.
1. שלוש השורות הראשונות מאחזרות את מרכז הגוף על כל ציר.
2. שלוש השורות הבאות מזיזות את הגוף לראשית הצירים.
3. הזימון ScaleX(factor); מבצע Zoom על כל משטחי האובייקט.
4. שלוש השורות האחרונות מחזירות את האובייקט למיקומו המקורי.
זו פונקציה קצרה אבל עתירת עבודה. פונקציות כאלה יכולות לגרום למשחק "לזחול". יש כאן "המון" לולאות (חֲשבו) ולכן קחו זאת בחשבון.
הפונקציה הבאה עושה עבודת סיבוב באותה הדרך:
void CModel::RotateSelfX(double alpha)
{
double centerx = GetCenterX();
double centery = GetCenterY();
double centerz = GetCenterZ();
TranslateX(-centerx);
TranslateY(-centery);
TranslateZ(-centerz);
RotateX(alpha);
TranslateX(centerx);
TranslateY(centery);
TranslateZ(centerz);
}
זהו רבותי. אני חושב שהסברתי את עצמי. גשו לעבודה והשלימו את מימוש המחלקה.
כשתסיימו נוכל לכתוב את פונקציית Main ולהריץ את הפרויקט. נוסיף את ממשק glut
ונוכל לבנות לנו קו ,עיגול, קובייה ולהציג אותם על המסך. משם עוד כמה טיפים וטריקים ותוכלו לבנות ,לבד , את המשחק שלכם.
הפונקציה main() וממשק הסביבה הגרפית
הפונקציה main() :
זוהי נקודת ההתחלה של כל יישום (*.exe). בשלב הריצה של התוכנית ,נקראת פונקציה זו ,ההוראות שבה מבוצעות זו אחר זו וסיומה הוא נקודת היציאה של התוכנית.
הוסיפו לפרויקט קובץ מסוג c++ File (.cpp) ותנו לו את השם ,למשל , MyMain .נוצר קובץ אחד MyMain.cpp ללא בן זוג *.h . כתבו בתוכו את הפקודות:
#include "init.h"
void main (int argc, char *argv[])
{
CInitGL MyGame(argc,argv);
}
קבצי הממשק הגרפי של OpenGL הם : init.h,init.cpp הקריאה להם באה מהפונקציה main() ולכן מבצעים #include לקובץ ההגדרות.
הפרמטרים שנשלחים אל main() הם מערך של מחרוזות, ומספר שלם argc – מספר המחרוזות. יש העושים בכך שימוש ומעבירים פרמטרים משורת הפקודה או ממאפייני הפרויקט. לנו אין צורך בכך. פשוט כדאי להכיר.
ההוראה היחידה בפונקציה : CInitGL MyGame(argc,argv);קוראת לקונסטרקטור של המחלקה CInitGL המוגדרת בקובץ init.h . התוצאה מופע בשם MyGame של המחלקה CInitGL וקריאה לפונקציה בשם זה :
CInitGL::CInitGL(int argc, char* argv[])
{
int x,y;//משתני ממדי החלון
glutInit(&argc, argv);//אתחול הסביבה הגרפית
x = glutGet(GLUT_SCREEN_WIDTH);//רוחב המסך בו רצה התוכנית
y = glutGet(GLUT_SCREEN_HEIGHT);//גובה המסך
glutInitWindowPosition(0,0);//מיקום החלון בפינה השמאלית העליונה
glutInitWindowSize(x,y);//חלון בכל המסך
glutInitDisplayMode( GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE );//אתחול צבעים
glutCreateWindow("MineNewPrj");//יצירת חלון בשם זה
SetBeckgroundColor(1,0,1);//צבע רקע לחלון
//כאן נבנה התהליך
InitParameters();//אתחול הפרמטרים של הממשק הגרפי
glutReshapeFunc( reshape );//ריענון המסך
glutKeyboardFunc( keyboard );//אירועי מקלדת, מקשים רגילים
glutSpecialFunc( special );//קלט מקשים מיוחדים
glutMouseFunc(mouseFunc);//אירועי מקשי עכבר
glutMotionFunc(mouseMovement);//מיקום סמן העכבר
glutPassiveMotionFunc(PssiveMotion);//תזוזה רציפה של סמן העכבר
glutDisplayFunc (MyDraw);//פונקציית הציור
glutMainLoop();//התהליך הראשי :Thread
}
נוצר תהליך אשר שלביו השונים(הפונקציות הכלולות בו) נקראים כל עוד התוכנית רצה.
את המושג תהליך יש להבין אינטואיטיבית. הוא כולל מספר פונקציות המבוצעות שוב ושוב ,אותן פונקציות, לאו דווקא באותו סדר. הפונקציה MyDraw תיקרא כל פעם מחדש ותצייר על המסך. הפונקציות מונחות האירועים, כמו קלט מקש או תנועת עכבר, רק אם הייתה פעולת מקלדת או עכבר.
הפונקציה היותר חשובה, מבחינתנו, היא MyDraw בתוכה מבוצע הציור בתלת מימד (של הפרויקט שלנו) בדו ממד (למשל טקסט) וריענון המסך.
חשוב להבין שהמסך עובר כל הזמן ריענון (מחיקה) וציור מחדש. ללא זה, המסך יופיע כאוסף של מריחות מהבהבות. בסביבות ישנות Turbo c/Pascal כל זאת היה באחריות התכנת. כאן glut עושה זאת עבורנו. קצת פחות חכם מפעם אך נוח ומאפשר להקדיש שֶכל למרכיבים היותר חשובים בעבודה. (glut עושה בשבילנו עוד "כמה דברים פעוטים..." כמו הסתרת משטחים כשנדרש, ואפשר לבנות בעזרתו אובייקטים מורכבים כפשוטים, אנחנו נבנה אותם בכוחנו לבד)
לפני שנמשיך עם הפונקציה MyDraw חובה לומר: הקובץ init(h/cpp) הוא לא המצאה שלי. ספק אם של מישהו. ניתן למצוא ממשק זה בדוגמאות רבות מספור ובגרסאות שונות. התכנת עושה copy/paste ומשנה לפי צרכיו.
פרויקטים פשוטים מכילים רק קובץ זה בתוספת הפונקציה main וצורות מובנות בספרית glut .מספר מניפולציות על הצורות , אף הן מהספרייה , ותוכלו לסחוט קריאות התפעלות. גרפיקה ממוחשבת לא תלמדו מכך.
אז הנה הפונקציה החשובה MyDraw() :
void MyDraw()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glPushMatrix();
glutPostRedisplay();
//==============================================
// ציור בתלת ממד
//manage game
//screen.ActivateScreen();//ציור המשחק
//==============================================
glPushMatrix();
setOrthographicProjection();
glLoadIdentity();
// ציור בדו ממד
//screen.DrawMyText();//ציור הטקסט
CInitGL::SetBeckgroundColor(0.53,0.91,1);//קביעת צבע הרקע
resetPerspectiveProjection();
glPopMatrix();
glPopMatrix();
glutSwapBuffers();
}
ההוראה הראשונה מנקה את החוצץ – buffer הראשון, ומכינה אותו (בזיכרון) לכתיבת ההוראות שבהמשך, או בפשטות עורכת את תמונת המסך הבאה. בינתיים מוצג על המסך תוכנו של החוצץ השני. ההוראה האחרונה מחליפה בין החוצצים. וכך מתקבלת תמונה חדשה על המסך. וחוזר חלילה ,כמו שאומרים, שוב ושוב.
בין שתי ההוראות מבוצעת פעולת מטריצות שלא נדון בה. כן נפרט את פעולות האובייקט
screen .הראשונה מנהלת את המשחק והשניה מציירת טקסט ,הכל על המסך. כדי להגיע לאובייקט זה עלינו לבנות מחלקה לטיפול בהוראות מסך ,ובתוכה להכיל אובייקט מהמחלקה לניהול המשחק וכן את כל אובייקטי הציור והמניפולציות עליהם. מכאן, שעלינו לבנות אובייקטים אלו ורק אז להמשיך עם הכתוב לעיל. וזאת נעשה מיד :נבנה מחלקות של עיגול, קובייה ואז נמשיך. רק לפני כן נסביר עוד קצת על המחלקה CInitGL והרכבה.
#pragma once
#include <GL/glut.h>
class CInitGL
{
private:
public:
CInitGL();
virtual ~CInitGL();
CInitGL(int argc,char *argv[]);
void InitParameters();
static void SetBeckgroundColor(double r, double g, double b);
};
void special(int key, int x, int y);
void keyboard(unsigned char key, int x, int y);
void setOrthographicProjection();
void resetPerspectiveProjection();
void renderBitmapString(double x, double y, void *font,char *string);
void renderSpacedBitmapString(double x, double y,int spacing, void *font,char *string);
void renderVerticalBitmapString(double x, double y, int bitmapHeight, void *font,char *string);
void MyDraw();
void reshape( GLsizei width, GLsizei height );
void special(int key, int x, int y);
void keyboard(unsigned char key, int x, int y);
void mouseFunc(int button,int state,int x,int y);
void mouseMovement (int mx, int my);
void PssiveMotion(int x, int y);
מבנה מוזר מעט. מצאתי אוסף של פונקציות glut חלקן במבנה מונחה עצמים. אז הקובץ init.h בנוי חלקו בסגנון c++ וחלקו c .האם זה אפשרי ? עובדה.
במחלקה שני קונסטרקטורים, דסטרקטור אחד ושתי פונקציות בגישת (oop) מונחית עצמים
שאר הפונקציות ,לא פחות חשובות אולי אף יותר, מוגדרות בקובץ, לא במחלקה.
חפשו אותן ברחבי הרשת והעתיקו אותן לקבצים שלכם. אין טעם להקליד במו ידיכם פקודות לא מובנות ולכן לא צירפתי אותן. יש טעם לחפש הסברים ולקרוא ולהבין אותם. אנו נפרט את רוב הפונקציות ברגע שנידרש להן. בינתיים הקלידו קמפלו ושימרו. אפשר גם להריץ את הפרוייקט ,יפתחו שני מסכים: האחד של dos והשני של OpenGL בו יוצג המשחק.
אז זהו רבותי. עד כאן פעולות הכנה שכללו את הסביבה visual studio את השפה c++ ומעט glut של OpenGL .בהמשך נבנה מחלקות של גופים ,בעיקר בתלת ממד, נלמד לשלב אותם ולשלב ביניהם, עד שנקבל משחק מחשב.