המרכיב הקטן ביותר של כל אובייקט גרפי במישור ובמרחב היא הנקודה.מלבד המימדים של הנקודה הנקבעים ע"י סביבת העבודה אנו מאפיינים את הנקודה בעזרת שניים:
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;
}
בעקבות ההוראה 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 .אין הבדל מבחינתנו.