مشروع: تطبيق Ray Tracing باستخدام C‎#‎ – الحلقة 1 – الأساسيات البحتة « مغامرات برمجية

مشروع: تطبيق Ray Tracing باستخدام C‎#‎ – الحلقة 1 – الأساسيات البحتة

الرسوم ثلاثية الأبعاد بواسطة الكمبيوتر هو موضوع مشوق وله شجون. لذا اخترته ليكون الموضوع الاستهلالي لتصنيف جديد في المدونة، أنوي من خلاله تقديم مشاريع كاملة “من طق طق لسلام عليكم” كمجموعة دروس تطبيقية. المشروع التالي سيكون تطبيقاً لما يسمى بالـRay Tracing باستخدام لغة C‎#‎. لماذا C‎#‎ بالذات؟ لا يوجد سبب معين. مزاجي كان ميالاً نحو C‎#‎ عندما بدأت المشروع. ولكن ما سأقدمه هنا يمكن بسهولة تطبيقه في أي لغة. الصورة بالأعلى هي نتيجة لعملية Ray Tracing باستخدام برنامجي.

في هذه الحلقة الأولى سوف أغطي الأساسيات البحتة فقط. وفي كل حلقة بعد ذلك سأضيف المزيد والمزيد من الخصائص ونعدل في البرنامج تباعاً. تحذير بسيط: مثل أي موضوع له علاقة بالرسوم الثلاثية الأبعاد سوف نغوص كثيراً في الرياضيات. ولكن في غالب الوقت لن تتعدى الرياضيات التي سنستخدمها حساب المتجهات والهندسة الإقليدية ومواضيع أساسية مثل هذه.

لنبدأ بالسؤال الأساسي. ما هو الـRay Tracing؟

يمكن ترجمة هذا المصطلح بـ”تتبع الإشعاعات”. قد يبدو اسماً مخيفاً وثقيل الدم، ولكنه منطقي جداً وبشرح ما تقوم به العملية كما سنرى الآن. من أجل خاطر توحيد المصطلحات سأبقى على استخدام المصطلح الإفرنجي.

يمكننا تعريف الـRay Tracing ببساطة بأنه توليد صور المجسمات الثلاثية الأبعاد عن طريق محاكاة عمل العين البشرية. قام العالم المسلم ابن الهيثم في كتابه المشهور “كتاب المناظر” لأول مرة بشرح حاسة البصر بشكل صحيح. الصورة التي نراها عن طريق أعيننا ما هي إلا إشعاعات الضوء المنعكسة من الأجسام المرئية إلى عدسة عيننا.

الصورة بأعلاه هو تمثيل بسيط لهذه النظرية. مصادر الضوء (الشمس والهلال في الصورة) تطلق عدد هائل من إشعاعات الضوء المستقيمة. في الصورة تم رسم بعض هذه الإشعاعات باللون الأسود. هذه الإشعاعات تنعكس من الأجسام المرئية، وتختلف طبيعة هذا الانعكاس باختلاف الأجسام من حيث شكلها وخامتها. بعض هذه الإشعاعات المنعكسة ستصل إلى أعيننا (وهي ممثلة باللون الأخضر) وهي الإشعاعات التي سنراها وتكون الصورة الموجودة في دماغنا. وبعضها لن تصل إلى أعيننا (وهي ممثلة باللون الأحمر) ولن نراها. لهذا السبب لا نرى جزئاً من الكرة المخبأة وراء المكعب.

الـRay Tracing تحاول محاكاة هذه العملية لتوليد صور فائقة الواقعية لأنه يمكننا بهذه الطريقة تضمين أشياء مثل الانعكاسات وانكسار الضوء. قد يخطر في بال البعض أن هذه هي الطريقة التي تستخدمها الألعاب ثلاثية الأبعاد. ولكن هذا ليس صحيحاً. الـRay Tracing عملية مكلفة جداً وبطيئة. لذا من غير الممكن (حالياً) استخدامها لتوليد الصور ثلاثية الأبعاد في الوقت الحقيقي. ما تستخدمه الألعاب هو أسلوب آخر يدعى Rasterization. هو أقل واقعية (لأنه لا يحاول محاكاة ما يحدث في الحقيقة) ولكنه سريع جداً. ولكن الـRay Tracing يمكن استخدامه لتوليد الأفلام ثلاثية الأبعاد، وهو ما يحدث غالباً.

إذن الـRay Tracing هو عملية تتبع هذه الإشعاعات لمعرفة أيها يدخل إلى العين وما لونها. لكننا لا نحتاج تتبعها كلها. هناك عدد شبه لا نهائي من إشعاعات الضوء حوالينا، ولن يصل إلى أعيننا سوى كسر بسيط من هذا العدد. لذا من المنطقي (وأقل تكلفة) أن نتتبع فقط تلك الإشعاعات التي تصل إلى عيننا. كيف نعرف ما هي هذه الإشعاعات؟ عن طريق رسم خط مستقيم من العين إلى الخارج (رغم أن نظرية ابن الهيثم تنص بأن الإشعاعات تنطلق من الخارج إلى العين، ولكن لتسهيل التطبيق أحكامه).

في الشكل بعاليه هو الـRay Tracing بأبسط أشكاله. لدينا الجسم المرئي في اليمين. العين (أو الكاميرا كما سنسميها لاحقاً) في اليسار. الخط الذي يصل بينهما هو الشعاع الذي يمثل الضوء المنعكس من الجسم إلى العين. الشبكة في المنتصف تمثل الصورة ثنائية الأبعاد التي نراها والتي سيولدها الـRay Tracing. كما ترون هي عبارة عن شبكة من البيكسلات. البيكسل الملون بالأحمر هو البيكسل الذي مر من خلاله الشعاع. وسيكون لون هذا البيكسل نفس لون الكرة في نقطة اصطدامها بالشعاع (ممثل باللون الأحمر). لتوليد صورة كاملة نطلق شعاعاً من العين إلى كل بيكسل في الصورة ونتتبعه لمعرفة الشئ الذي يصطدم به في النهاية لمعرفة لون هذا البيكسل.

لتسهيل الموضوع لنبسطه إلى شكل ثنائي البعد:

مرة أخرى الكامير في اليسار والأجسام في اليمين والصورة في المنتصف. هناك ستة بيكسلات، أطلقنا شعاعاً من الكاميرا إلى كل بيكسل فيهم وتتبعناه إلى النهاية. من الأعلى إلى الأسفل، الأول لم يصب شيئاً لذا لا يوجد لون (ولو أننا في الغالب سنضع لون خلفية)، الثاني ضرب الكرة الحمراء لذا نأخذ اللون الأحمر، الثالث ضرب الكرة الزرقاء لذا لونّا البيكسل بالأزرق، والرابع مثله، والخامس ضرب الصندوق الزهري (أعتقد أن هذا زهري على كل حال)، والأخير لم يضرب شيئاً.

لاحظ أن الشعاع الثالث لو أننا أكملنا تتبعه بعد الكرة الزرقاء كان سيضرب الكرة الحمراء، ولكن لأننا وصلنا للأزرق أولاً تجاهلنا الكرة الحمراء وأخذنا اللون الأزرق. بشكل رياضي سنقول أن طول الشعاع من الكاميرا مروراً بالبيكسل الثالث إلى الكرة الزرقاء أقصر من طول الشعاع من الكاميرا مروراً بالبيكسل الثالث إلى الكرة الحمراء.

أصبح الآن لدينا Ray Tracing بدائي، ولكنه يفي بالغرض اليوم. لنبدأ التطبيق!

التطبيق الذي سنبنيه سيكون على شكل Library ليسهل استخدامه مع أي برنامج. لن يكون سريعاً جداً. هدفي هنا هو الوضوح وليس الأداء. لنبدأ بأبسط الأجزاء ثم نبني عليها.

أبسط وحدات البرنامج هو تمثيل الإحداثيات ثلاثية الأبعاد والمتجهات. كلاهما يتكون من ثلاث قيم رقمية وحساباتها متشابهة. لذا سنمثلهما معاً في struct سأسميه Vector3D. بالإضافة لتخزين الثلاث قيم التي تكونه، هناك عمليات حساب المتجهات. لن أخوض فيها كثيراً. أي مرجع بسيط في حساب المتجهات يفي بالغرض. هذا الدرس من أكاديمية خان بداية جيدة. معظم هذه الدوال لن نستخدمها في درس اليوم، ولكنها ستمر علينا لاحقاً.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
    public struct Vector3D
    {
        //Properties
        public double x { get; set; }
        public double y { get; set; }
        public double z { get; set; }

        //Constructor      
        public Vector3D(double X, double Y, double Z):this()
        {
            x = X;
            y = Y;
            z = Z;
        }

        //Dot product of two vectors
        public static double operator *(Vector3D a, Vector3D b)
        {
            return (a.x * b.x) + (a.y * b.y) + (a.z * b.z);
        }

        //Cross product of two vectors
        public static Vector3D Cross(Vector3D a, Vector3D b)
        {
            var x = a.y * b.z - b.y * a.z;
            var y = a.z * b.x - b.z * a.x;
            var z = a.x * b.y - b.x * a.y;
            return new Vector3D(x, y, z);
        }

        //Vector arithmatic
        public static Vector3D operator *(double scalar, Vector3D vector)
        {
            var xd = scalar * vector.x;
            var yd = scalar * vector.y;
            var zd = scalar * vector.z;

            return new Vector3D(xd, yd, zd);
        }
        public static Vector3D operator *(Vector3D vector, double scalar)
        {
            return scalar * vector;
        }                
        public static Vector3D operator -(Vector3D start, Vector3D finish)
        {
            var xd = start.x - finish.x;
            var yd = start.y - finish.y;
            var zd = start.z - finish.z;

            return new Vector3D(xd, yd, zd);
        }
        public static Vector3D operator +(Vector3D start, Vector3D finish)
        {
            var xd = start.x + finish.x;
            var yd = start.y + finish.y;
            var zd = start.z + finish.z;

            return new Vector3D(xd, yd, zd);
        }

        //Distance between two coordinates
        public static double Distance(Vector3D start, Vector3D finish)
        {
            var xd = start.x - finish.x;
            var yd = start.y - finish.y;
            var zd = start.z - finish.z;

            return Math.Sqrt(xd * xd + yd * yd + zd * zd);
        }
                       
        //Returns magnitude of a vector
        public double Magnitude()
        {
            return Math.Sqrt(x * x + y * y + z * z);
        }

        //Returns a normalized vector (i.e. make it's magnitude equal to 1)
        public Vector3D Normalize()
        {
            var a = this.Magnitude();
            if (a == 0)
                return this;
            else
                return new Vector3D(x / a, y / a, z / a);
        }

        //Returns length of a vector
        public double Length()
        {
            return Math.Sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
        }
    }

وحدة أخرى سنحتاجها هي اللون. هي الأخرى يتم تمثيلها بثلاث قيم رقمية تمثل الألوان الأحمر والأخضر والأزرق. لكن الجديد هنا هو أن هذه القيم ستكون من 0 إلى 1 (وليس 0 إلى 255 كما قد يتوقع البعض). السبب هو أننا سنقوم بالكثير من الحسابات على الألوان (في درس مقبل) لذا يفضل أن تكون القيم Normalized، أي تكون القيمة العليا هي 1. إذا قرأتم كود Vector3D ستجدون دالة Normalize لأننا سنستخدمها أيضاً في حساب المتجهات. التالي هو كود struct سميناه Color لتمثيل الألوان. هناك بضعة دوال حسابية بسيطة ودالة لتحويل هذا اللون إلى النوع System.Drawing.Color لنستخدمه لاحقاً عندما نبني الصورة نفسها.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    public struct Color
    {
        //Value of red
        public double R { get; set; }

        //Value of green
        public double G { get; set; }

        //Value of blue
        public double B { get; set; }

        //Constructors      
        public Color(double r, double g, double b) : this()
        {
            R = CorrectValue(r);
            G = CorrectValue(g);
            B = CorrectValue(b);
        }

        //Convert to 32-bit RGB color
        public System.Drawing.Color ConvertTo32Bit()
        {
            var r = (int)Math.Ceiling(R * 255);
            var g = (int)Math.Ceiling(G * 255);
            var b = (int)Math.Ceiling(B * 255);

            return System.Drawing.Color.FromArgb(r, g, b);
        }

        //Function to make sure the values are always between 0 and 1 inclusive.
        private double CorrectValue(double a)
        {
            if (a < 0)
            {
                return 0;
            }
            else if (a > 1)
            {
                return 1;
            }
            else
            {
                return a;
            }
        }

        //Color arithmatic
        public static Color operator +(Color a, Color b)
        {
            return new Color(a.R + b.R, a.G + b.G, a.B + b.B);
        }
        public static Color operator -(Color a, Color b)
        {
            return new Color(a.R - b.R, a.G - b.G, a.B - b.B);
        }
        public static Color operator *(Color a, Color b)
        {
            return new Color(a.R * b.R, a.G * b.G, a.B * b.B);
        }
        public static Color operator *(double x, Color C)
        {
            return new Color(C.R * x, C.G * x, C.B * x);
        }
        public static Color operator *(Color C, double x)
        {
            return x * C;
        }
    }

الصورة الممثلة بالشبكة بين الكاميرا والمشهد تدعى View Plane (مستوى العرض). وهي طبعاً مكونة من مجموعة بيكسلات. سنمثل البيكسل بكلاس هذه المرة. السبب في عدم استخدام struct كما في الوحدتين السابقتين هو أنني أرى تمثيل البيكسل ككائن بحد ذاته (بدلاً من كونه مجرد قيمة نستخدمها مرة أو مرتين) سيكون أفضل على المدى البعيد. هناك خاصيتان لكل بيكسل: لون البيكسل وإحداثياته ثلاثية البعد في عالم المشهد. دعنا نتوقف عند هذه النقطة الأخيرة. نحن تعودنا على البيكسل بكونه له إحداثيات ثنائية البعد x و y داخل الصورة. وهذا صحيح. ولكن هذا البيكسل هو شئ معلق في العالم ثلاثي الأبعاد الذي يصف المشهد، وإذا أردنا أن نرسم خطاً ينطلق من الكاميرا ويمر به فإننا سنحتاج لمعرفة إحداثيات البيكسل “الحقيقية”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    public class Pixel
    {
        //Color of the pixel
        public Color PixelColor { get; set; }

        //Coordinates of the pixel in real world coordinates
        public Vector3D RealCoordinates { get; set; }

        //Constructor
        public Pixel(Color pixelColor, Vector3D coordinates)
        {
            PixelColor = pixelColor;
            RealCoordinates = coordinates;
        }
    }

بما أنه الآن لدينا البيكسل، سنعمل كلاس للـView Plane. خصائصه هي الطول والعرض (بالبيكسلات) ومصفوفة ثنائية البعد لاحتواء البيكسلات. سأضيف أيضاً دالة لتنتج صورة bitmap من هذه البيكسلات.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    public class ViewPlane
    {
        //Width of the view in pixels
        public int Width { get; set; }

        //Height of the view in pixels
        public int Height { get; set; }
               
        //Two dimensional array of all pixels in the view
        public Pixel[,] PixelArray {get; set;}

        //Constructor
        public ViewPlane(int width, int height)
        {
            Width = width;
            Height = height;

            PixelArray = new Pixel[width,height];
        }

        //Export to a GDI bitmap
        public System.Drawing.Bitmap ExportImage()
        {
            var picture = new System.Drawing.Bitmap(this.Width, this.Height);

            for (int x = 0; x < this.Width; x++)
            {
                for (int y = 0; y < this.Height; y++)
                {
                    picture.SetPixel(x, y, PixelArray[x, y].PixelColor.ConvertTo32Bit());
                }
            }

            return picture;
        }
    }

كائن أساسي آخر هو الشعاع. أساسي وبسيط جداً. له خاصيتان فقط: إحداثيات نقطة البداية والاتجاه.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    public class Ray
    {
        private Vector3D _direction;

        //Coordinates of the origin point of the ray
        public Vector3D Origin { get; set; }

        //The direction vector of the ray
        public Vector3D Direction {
            get
            {
                return _direction;
            }
            set
            {
                //Always normalize the direction vector
                _direction = value.Normalize();
            }
        }

        //Constructor
        public Ray(Vector3D origin, Vector3D direction)
        {
            Origin = origin;
            Direction = direction;
        }
    }

لدينا أيضاً عدد من الثوابت الرياضية، لذا سيكون من الجيد وضعها في مكان واحد نستطيع الوصول إليه بسهولة من بقية البرنامج. حالياً عرفنا قيمتان: اللانهاية و epsilon. اللانهاية لا تحتاج إلى شرح، ولو أنه بالنسبة لحساباتنا يمكننا أن نكتفي بأي عدد كبير جداً. ولكن لحسن الحظ دوت نت توفر لنا قيمة جاهزة للانهاية، لذا سنستخدمها. epsilon هو عدد صغير جداً نستعيض به عن الصفر في بعض الحسابات (في درس مقبل). السبب هو أن دقة الأرقام في أي برنامج حاسوبي محدودة. لذا أحياناً تكون المقارنة بالصفر غير ممكنة، مما يسبب بعض الأخطاء في الرسم. لذا نستعيض عنه بالقيمة 0.0001 وهي قيمة نتجت بعد تجربة بضعة قيم لنجد القيمة المناسبة.

1
2
3
4
5
6
7
8
9
    public class Globals
    {
        //Value to represent infinite depth
        public static double Infinity = double.PositiveInfinity;

        //Value to reprsent a very small value used in some calculation to avoid graphic
        //artifacts due to number precision errors.
        public static double Epsilon = 0.0001d;
    }

عندما نتتبع شعاعاً فالنتيجة إما عدم الاصطدام بشئ، أو الاصطدام بجسمٍ ما ووقتها نريد أن نعرف معلومات عن هذا الجسم لنعرف نتيجة عملية التتبع. هذه المعلومات سنخزنها في كائن سنسمية Collision. له ثلاث خواص: قيمة بوليان نخزن فيها إذا كان هناك اصطدام أم لا، مؤشر إلى الجسم الذي اصطدمنا به، والمسافة التي قطعها الشعاع للوصول إلى هذا الجسم.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public class Collision
    {
        //Has the ray hit anything?
        public bool IsHit { get; set; }

        //What did the ray hit?
        public IPrimitive HitObject { get; set; }        

        //Distance from origin of the ray to the intersection point
        public double Distance { get; set; }

        //Constructor
        public Collision(bool isHit) //Default constructor used to resent collision
        {
            IsHit = isHit;
            HitObject = null;
            Distance = Globals.Infinity;
        }

        public Collision(bool isHit, IPrimitive hitObject, double distance)
        {
            IsHit = isHit;
            HitObject = hitObject;
            Distance = distance;
        }
    }

الأجسام الثلاثية الأبعاد ستكون مكونة من خامة ما تصف لنا كيف تظهر. حالياً لدينا خاصية واحدة فقط هي اللون، أو بمصطلحات الـray tracing ما يسمى بالـdiffuse color. سنضع هذه الخاصية في كلاس Material ولاحقاً عندما يصبح برنامجنا أكثر تعقيداً سنضيف المزيد من الخواص.

1
2
3
4
5
6
7
8
9
10
11
    public class Material
    {
        //The diffuse color of the material
        public Color DiffuseColor { get; set; }

        //Constructor
        public Material(Color diffuseColor)
        {
            DiffuseColor = diffuseColor;
        }
    }

لنخوض قليلاً في شئ أكثر تعقيداً وملموس أكثر: الأجسام ثلاثية الأبعاد الأساسية، أو ما يسمى بـ3D Primitives. طبعاً هناك عدد شبه لا نهائي من الأشكال المختلفة. ولكنها كلها (من وجهة نظر الـRay Tracing) يجب أن تكون لها خاصية تبين الخامة المكونة منها، ودالة ندخل فيها قيمة الشعاع وترد لنا إذا حدث اصطدام مع هذا الجسم أم لا. وبما أن هناك شئ موحد سيكون من الأفضل أن نستفيد من خواص الـobject oriented programming ونضع هذا الشكل الموحد على شكل interface سنسميه IPrimitive متبعين العرف في التسمية بوضع حرف I في البداية.

1
2
3
4
5
6
7
8
    public interface IPrimitive
    {
        //The material used for the 3D primitive
        Material PrimitiveMaterial { get; set; }

        //Takes a Ray object, and checks to see if it intersects with the 3D primitive
        Collision Intersect(Ray ray);
    }

باستخدام هذا الـinterface سنعرّف الأجسام التي سنتعامل معها. في درس اليوم سنتعامل مع شكل واحد فقط هو الكرة. السبب هو أن هذا الشكل سهل التعامل معه ويبين في نفس الوقت جميع خصائص الـray tracing. الكرة أو sphere لها خاصيتان: إحداثي نقطة المنتصف، ونصف القطر. وبما أننا سنستخدم IPrimitive علينا وضع تعريف للدالة Intersect. مرة أخرى، لن أخوض في الرياضيات الخاصة في معرفة حدوث اصطدام مع شعاع. لمن يحب معرفة الجانب الرياضي يمكنكم قراءة هذه المقالة البسيطة. إذا اطلعتم على الكود بدقة ستجدون أن هناك تفريق فيما إذا كان الشعاع انطلق من خارج الكرة أم من داخلها. حالياً لن نستخدم هذه الخاصية، ولكنها ستفيدنا فيما بعد عندما نحسب انكسار الضوء في الأجسام الشفافة (كما هو موجود في الصورة بأعلى التدوينة).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    public class Sphere : IPrimitive
    {
        //Coordinates of the center of the sphere
        public Vector3D Center { get; set; }

        //The radius of the sphere
        public double Radius { get; set; }

        //Constructor
        public Sphere(Vector3D center, double radius)
        {
            Center = center;
            Radius = radius;
        }

        public Material PrimitiveMaterial { get; set; }

        //Intersection formula was taken from http://wiki.cgsociety.org/index.php/Ray_Sphere_Intersection
        public Collision Intersect(Ray ray)
        {
            var A = ray.Direction * ray.Direction;
            var B = 2 * (ray.Origin - Center) * ray.Direction;
            var C = (ray.Origin - Center) * (ray.Origin - Center) - Radius * Radius;

            var d = B * B - 4 * A * C;
            if (d < 0)
                return new Collision(false);    //No collision. Ray does not hit sphere.

            var sqrtD = Math.Sqrt(d);

            double q;
            if (B < 0)
                q = (-B - sqrtD) / 2;
            else
                q = (-B + sqrtD) / 2;

            var t0 = q / A;
            var t1 = C / q;

            if (t0 > t1)
            {
                var temp = t0;
                t1 = t0;
                t0 = temp;
            }

            if (t1 < 0)
                return new Collision(false);    //No collision. Sphere is behind origin point.

            if (t0 < 0)
            {
                //Origin inside sphere                
                var hitPoint1 = ray.Origin + ray.Direction * t1;
                return new Collision(true, this, t1);   //Collision. Origin point is inside sphere.
            }

            var hitPoint0 = ray.Origin + ray.Direction * t0;
            return new Collision(true, this, t0);       //Collision. Origin point is outside sphere.
        }

    }

لا يمكن أن يكون هناك مشهد من غير مشاهد، لذا سنعرف الكاميرا. مثل الأجسام الثلاثية الأبعاد، هناك عدة أنواع من الكاميرات. لكنها كلها تشترك في أنها تحتوي على دالتين. دالة لتوليد الـView plane في العالم الثلاثي الأبعاد عندما نعطيها طوله وعرضه بالبيكسلات وحجم البيكسل الواحد بوحدات العالم الحقيقي، ودالة لتوليد الإشعاعات الصادرة من الكاميرا إلى بيكسل معين. لذا سنبني interface مرة أخرى سنسميه ICamera هكذا:

1
2
3
4
5
6
7
8
    public interface ICamera
    {
        //Creates a view plane as seen by the camera with the following pixel width and height and size of a pixel in real world units
        ViewPlane CreateViewPlane(int width, int height, double PixelSize);

        //Creates a ray that passes through the target pixel
        Ray CreateRay(Pixel targetPixel);
    }

كاميرتنا ستكون بسيطة جداً. ستكون perspective camera أي كاميرا منظور، وهو مصطلح يطلق على كاميرا تنطلق فيها الإشعاعات من نقطة واحدة. أي تماماً مثل العين البشرية أو الكاميرا العادية، بالضبط مثل الرسومات التي استخدمناها حتى الآن. سنبسطها بأننا سنثبت مكان العين في الإحداثي (0,0,-5) وسيكون الـview plane أمامه بالضبط ومنتصفه في النقطة (0,0,0) وسيكون موازياً للمستوى x,y أي أن قيمة z في الـview plane ستكون دائماً صفر. أخبرتكم أن هناك الكثير من الرياضيات :) !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    public class VerySimpleCamera : ICamera
    {
        //Location of the camera is always fixed
        private readonly Vector3D CameraLocation = new Vector3D(0, 0, -5);

        //Calculate the view plane for our simple fixed camera
        public ViewPlane CreateViewPlane(int width, int height, double PixelSize)
        {
            //Calculate the width and height in real world units
            var realWidth = width * PixelSize;
            var realHeight = height * PixelSize;

            //The center of the view plane is always at (0,0,0) with the z-axis always fixed at 0
            //Calculate the coordinate of the top left corner of the view plane
            //Remeber that left is negative X, and up is positive Y
            var topLeft = new Vector3D(-realWidth / 2, realHeight / 2, 0);

            //Create the view plane, and assign real coordinates to all pixels
            var view = new ViewPlane(width, height);

            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    //Calculate real world coordinate and assign it to the pixel with the color black
                    var realCoordinate = topLeft;
                    realCoordinate.x+= x * PixelSize;
                    realCoordinate.y-= y * PixelSize;

                    view.PixelArray[x, y] = new Pixel(new Color(), realCoordinate);
                }
            }

            return view;
        }

        public Ray CreateRay(Pixel targetPixel)
        {
            //Calculate direction from camera location to the target pixel
            var direction = targetPixel.RealCoordinates - CameraLocation;

            //Create ray object
            var cameraRay = new Ray(CameraLocation, direction);

            return cameraRay;
        }

    }

لدينا الأجسام ولدينا الكاميرا. حان الوقت لنضعها معاً لنكون “مشهداً” أو scene. الكلاس التالي يستخدم لتوصيف مشهد. نخزن فيه نوع الكاميرا التي سنستخدمها، وقائمة بالأجسام الموجودة في المشهد، ولون الخلفية.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public class Scene
    {
        //The camera used to render the scene
        public ICamera SceneCamera { get; set; }

        //List of all 3D primitives in the scene
        public List<IPrimitive> ScenePrimitives { get; set; }

        //Background color of the secne
        public Color BackgroundColor { get; set; }

        //Constructor
        public Scene(ICamera camera, List<IPrimitive> primitives, Color backgroundColor)
        {
            SceneCamera = camera;
            ScenePrimitives = primitives;
            BackgroundColor = backgroundColor;
        }
    }

وآخراً وليس أخيراً نصل إلى كود الـray tracing نفسه! سنضعها في كلاس منفصل سنسميه RayTracer ولدينا عدة دوال سأشرحها بالتفصيل. لدبنا طبعاً تعريف الكلاس والـconstructor الخاص به.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public class RayTracer
    {
        //The scene object to be rendered
        private Scene SceneToRender { get; set; }

        //Width of the result view plane in pixels
        public int Width { get; set; }

        //Height of the result view plane in pixels
        public int Height { get; set; }

        //Size of the pixel in real world units
        public double PixelSize { get; set; }

        //Constructor
        public RayTracer(Scene sceneToRender, int width, int height, double pixelSize)
        {
            SceneToRender = sceneToRender;
            Width = width;
            Height = height;
            PixelSize = pixelSize;
        }
.
.
.
    }

ثم لدينا الدالة الرئيسية Render التي نناديها لتقوم بتوليد الـview من الـscene الذي عرفناه. تقوم هذه الدالة باستخدام الكاميرا الخاصة بالمشهد لتوليد view plane فارغ. ثم نمر على جميع البيكسل الموجودة في الـview plane ونكون شعاعاً من الكاميرا إلى البيكسل ونتتبعه بواسطة الدالة RayTrace التي تقوم بتغيير قيمة هذا البيكسل حسب نتيجة الدالة. في النهاية سيكون لدينا view plane يصف المشهد كما تراه الكاميرا.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        //The main rendering function. Takes a scene object, and the width, height and pixel size of result view plane
        public ViewPlane RenderScene()
        {
            //Create the blank view plane as seen by the camera
            var resultView = SceneToRender.SceneCamera.CreateViewPlane(Width, Height, PixelSize);

            //Iterate through all the pixels and ray trace trough them
            for (int x = 0; x < Width; x++)
            {
                for (int y = 0; y < Height; y++)
                {
                    var targetPixel = resultView.PixelArray[x, y];
                   
                    //Create ray from camera that passes through the pixel
                    var cameraRay = SceneToRender.SceneCamera.CreateRay(targetPixel);

                    //Ray trace
                    RayTrace(cameraRay, targetPixel);
                }
            }
            return resultView;
        }

وهنا لدينا كود الدالة RayTrace. وهي تقوم بأخذ الشعاع والبيكسل، ثم تقوم بتتبع الشعاع باستخدام الدالة Trace التي ينتج عنها كائن اصطدام Collision. إذا كان هناك اصطدام أي IsHit == true نقوم بتغيير لون البيكسل إلى لون الجسم الذي اصطدمنا به. وإذا لم يكن هناك اصطدام نغير لون البيكسل إلى لون الخلفية.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        //Main ray tracing function. Takes a ray and the target pixel
        //Changes the contents of pixel according to the result of the ray tracing.
        private void RayTrace(Ray ray, Pixel TargetPixel)
        {
            //Trace the ray
            var cameraCollision = Trace(ray);

            if (cameraCollision.IsHit)  //The ray has hit an object
            {
                //Set the pixel's color to be the same as the primitive we hit
                TargetPixel.PixelColor = cameraCollision.HitObject.PrimitiveMaterial.DiffuseColor;
            }
            else //The ray did not hit anything
            {
                //Set the pixel's color to be the background color
                TargetPixel.PixelColor = SceneToRender.BackgroundColor;
            }
        }

وأخيراً لدينا كود الدالة Trace التي تأخذ كائن الشعاع. نقوم أولاً بافتراض أنه لا يوجد هناك أي اصطدام في المتغير rayCollision. وسنعرف متغير اسمه minDistance نحتفظ فيه بأقصر مسافة وصل إليها الشعاع، وسنضع قيمته الابتدائية بأنها لا نهاية. ثم تقوم بالمرور على كل جسم موجود في المشهد وندخل الشعاع في الدالة Intersect الخاصة به. إذا حدث هناك اصطدام ننظر إلى المسافة حتى نقطة الاصطدام. إذا كانت هذه المسافة أقل من قيمة minDistance نغير قيمة minDistance إلى هذه المسافة الجديدة، ونغير قيمة rayCollision إلى الاصطدام الناتج عن عملية Intersect. في نهاية الـloop سنعرف ما إذا كان هناك اصطدام أم لا. وإذا كان هناك اصطدام سنعرف أي جسم هو الأقرب إلى الكاميرا (أي أنه الجسم الذي ستراه).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        //Tracing function. Calls the Intersect function on all primitives in the scene. Finds out if there is an intersection, and what object we hit
        private Collision Trace(Ray ray)
        {
            //Set the minimum distance to infinity
            var minDistance = Globals.Infinity;

            //Reset the collision
            var rayCollision = new Collision(false);

            //Iterate through all the primitives in the scene
            foreach (IPrimitive obj in SceneToRender.ScenePrimitives)
            {
                //Check if the ray intersects with the 3D primitive
                var collision = obj.Intersect(ray);

                if (collision.IsHit && collision.Distance < minDistance)    //intesection has occured, and the distance is less than the latest minimum distance
                {
                    //Set the new values
                    minDistance = collision.Distance;
                    rayCollision = collision;
                }
            }

            //Return the result
            return rayCollision;
        }

وهذه عملية ray tracing كاملة! بقي الآن أن نجرب إذا كانت تعمل أم لا. لذا عملت برنامج console بسيط يستخدم هذه الـLibrary. كان بإمكاننا طبعاً استخدامها في أي نوع من البرامج، ولكننا سنكتفي بالبساطة الآن.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    class Program
    {
        static void Main(string[] args)
        {
            //Timer to measure execution time
            var time = DateTime.Now;

            //Instantiate camera
            var camera = new VerySimpleCamera();

            //Create red sphere
            var redSphere = new Sphere(new Vector3D(0, 0, 2), 2);
            redSphere.PrimitiveMaterial = new Material(new Color(.8, .2, .2));

            //Create blue sphere behind it
            var blueSphere = new Sphere(new Vector3D(3, 1.5, 5), 2);
            blueSphere.PrimitiveMaterial = new Material(new Color(.1, .1, .7));

            //Add 3D objects to a list
            var objects = new List<IPrimitive>();
            objects.Add(redSphere);
            objects.Add(blueSphere);

            //Instantiate scene, using a very dark gray as the background color
            var scene = new Scene(camera, objects, new Color(.1, .1, .1));

            //Instantiate ray tracing engine to produce a 400 x 400 pixel image. Pixel size of 0.025 means the image will 10.0 x 10.0 in real world units.
            var engine = new RayTracer(scene, 400, 400, 0.025);

            //Render the scene
            var view = engine.RenderScene();

            //Create a GDI bitmap
            var bmp = view.ExportImage();

            //Save the image
            bmp.Save("output.bmp");

            Console.WriteLine("Ray tracing done. Execution time: {0:d} ms", (DateTime.Now - time).Milliseconds);
            Console.ReadKey();
        }
    }

كما ترون عرفنا الكاميرا، وكرتان بنفس الحجم الزرقاء فيهما وراء الحمراء (من وجهة نظر الكاميرا)، ووضعنا هذا كله في Scene. ثم عرفنا RayTracer لينتج لنا صورة حجم 400 × 400 بحجم بيكسل 0.025، وحفظنا الصورة في ملف. وهنا النتيجة:

أليس هذا أجمل شئ رأيتموه في حياتكم!!

.

.

.

.

.

.

حسناً …. أعرف أنها صورة بدائية جداً، ولكن تذكروا أننا في بداية الطريق هنا. لم نقم بحساب أشياء مثل الإضاءة والتظليل والانعكاسات والانكسارات و غيرها. ولكن دعنا نرى ما وصلنا إليه. الكرة الحمراء بالفعل غطت الكرة الزرقاء. ورغم أن حجم الكرتين واحد، إلا أن البعيدة (الزرقاء) بدت أصغر من القريبة (الحمراء) تماماً كما يحدث في الواقع رغم أننا لم نذكر أي شئ عن هذا في الكود. سبب هذه الواقعية (النسبية) هو أننا نحاكي ما يحدث في الطبيعة.

حسناً. يكفي اليوم. وإذا أحيانا ربنا سنلتقي في دروس تالية نحاول فيها، شيئاً فشيئاً، تطوير برنامجنا البسيط هذا للوصول إلى صور أكثر واقعية.

يمكنكم الحصول على كود هذا الدرس من هذا الرابط.

أو يمكنكم الحصول على آخر إصدارة من كود هذه السلسلة من الدروس من موقع Google Code هذا.


المقالات في هذه السلسلة:

Post to Twitter

3 تعليق - أضف تعليق
  1. شكرا جزيلا استاذي العزيز فعلا ازلت الكثير من الغموض الذي كان يخيم على هذا الموضوع

  2. Quality articles or reviews is the main to be a focus
    for the visitors to pay a visit the site, that’s what this website is providing.

  3. Eduardo قال:

    Help, I’ve been informed and I can’t become igtonarn.

أضف تعليق

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

*

يمكنك استخدام أكواد HTML والخصائص التالية: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


مرحباً , تاريخ اليوم هو الثلاثاء, 2017/02/21