418341 ภาคต้น 2552: การบ้าน 2
คำเืตือน: การบ้านนี้เป็นการบ้านที่ต้องเขียนโปรแกรมมาก และมีเวลาทำน้อย นอกจากนี้ยังต้องมีความรู้ภาษา C++ เป็นอย่างดี
ุถ้าคุณไม่มั่นใจว่าคุณมีความรู้ภาษา C++ ดีพอ คุณสามารถไปหาข้อมูลเพิ่มเติมได้จาก
นอกจากนี้ โค้ดที่ให้ไปในการบ้านนี้มีจำนวนมาก ผมได้อธิบายรายละิเอียดของโ้ด้ดพวกนี้ไปบางส่วนแล้วในเลคเชอร์วันที่ 19 และ 21 สิงหาคม การดู สไลด์ ประกอบการสอนจะช่วยคุณได้มาก
เนื้อหา
ดาวน์โหลดโค้ด
ขั้นแรกให้ดาวน์โหลดโค้ดจากงลิงนี้ก่อน: homework-02.zip หลัีงจากนั้นให้ขยายไฟล์แล้วเปิดไฟล์ด้วย homework-02.sln ด้วย Microsoft Visual Studio 2008 Express
ข้อ 1: Matrix
เมื่อเปิดไฟล์ homework-02.sln แล้วให้ปิด project ทั้งหมดแล้วเหลือ project เหล่านี้เอาไว้
- gtest
- problem0
- problem1
- problem1_test
โดยการปิดโปรเจคใ้้ห้ทำโดยคลิกขวาที่ชื่อโปรเจคแล้วเลือก Unload Project
หลังจากนั้นให้เปิด problem1.cpp ในโปรเจค problem1 แล้วเติมฟังก์ชันเหล่านี้ให้สมบูรณ์
Matrix4x4 Matrix4x4::identity()
- คืน identity matrix ขนาด 4 คูณ 4
Matrix4x4 Matrix4x4::translate(float x, float y, float z)
- คืน matrix ของการเลื่อนแกนขนานไปตามแกน x เท่ากับอาร์กิวเมนต์ x, ไปตามแกน y เท่ากับอาร์กิวเมนต์ y, และไปตามแกน z เท่ากับอาร์กิวเมนต์ z
Matrix4x4 Matrix4x4::translate_x(float x)
- คืน matrix ของการเลื่อนแกนขนานไปตามแกน x เท่ากับอาร์กิวเมนต์ x
Matrix4x4 Matrix4x4::translate_y(float y)
- คืน matrix ของการเลื่อนแกนขนานไปตามแกน y เท่ากับอาร์กิวเมนต์ y
Matrix4x4 Matrix4x4::translate_z(float z)
- คืน matrix ของการเลื่อนแกนขนานไปตามแกน z เท่ากับอาร์กิวเมนต์ z
Matrix4x4 Matrix4x4::scale(float x, float y, float z)
- คืน matrix ของการย่อขยายขนาดตามแกน x เท่ากับอาร์กิวเมนต์ x เท่า, ตามแกน y เท่ากับอาร์กิวเมนต์ y เท่า, และตามแกน z เท่ากับอาร์กิวเมนต์ z เท่า
Matrix4x4 Matrix4x4::scale_x(float x)
- คืน matrix ของการย่อขยายขนาดตามแกน x เท่ากับอาร์กิวเมนต์ x เท่า
Matrix4x4 Matrix4x4::scale_y(float y)
- คืน matrix ของการย่อขยายขนาดตามแกน y เท่ากับอาร์กิวเมนต์ y เท่า
Matrix4x4 Matrix4x4::scale_z(float z)
- คืน matrix ของการย่อขยายขนาดตามแกน z เท่ากับอาร์กิวเมนต์ z เท่า
Matrix4x4 Matrix4x4::rotate(float degrees, Vector3 axis)
- คืน matrix ของการหมุนเป็นมุม degrees องศา รอบแกน axis
Matrix4x4 Matrix4x4::rotate_x(float degrees)
- คืน matrix ของการหมุนเป็นมุม degrees องศา รอบแกน x
Matrix4x4 Matrix4x4::rotate_y(float degrees)
- คืน matrix ของการหมุนเป็นมุม degrees องศา รอบแกน y
Matrix4x4 Matrix4x4::rotate_z(float degrees)
- คืน matrix ของการหมุนเป็นมุม degrees องศา รอบแกน z
เมื่อเติมเสร็จเรียบร้อยแล้วให้ทดลอง compile ด้วยการกด F7
ถ้าคอมไพล์ผ่านให้เลือกโปรเจค problem1_test เป็น Startup Project โดยคลิกขวาที่ชื่อโปรเจค problem1_test แล้วเลือก Set as Startup Project หลังจากนั้นจึงลองรันมันด้วยการกด Ctrl+F5 ถ้าโปรแกรมคุณทำงานได้ถูกต้อง คุณจะผ่านการทดสอบที่เขียนไว้ใน problem1_test.cpp
ข้อ 2: Transform
หลังจากทำข้อ 1 เสร็จแล้ว ให้เปิดโปรเจค problem2 และ problem2_test ด้วยการคลิกขวาที่ชื่อโปรเจคแล้วเลือก Load Project
ในข้อ 2 นี้เราจะทำการ implement ฟังก์ชันสำหรับสร้าง instance ของคลาส Transform ซึ่งใช้แทน affine transformation ที่สำคัญใน computer graphics
คลาส Transform มีส่วนประกอบเป็น matrix ขนาด 4 คูณ 4 สาม matrix ได้แก่
- m เป็น matrix ของ transform
- mi เป็น inverse ของ m
- mit เป็น inverse transform ของ m (กล่าวคือเป็น transpose ของ mi)
ฟังก์ชันที่ต้ัองเติมให้สมบูรณ์อยู่ในไฟล์ชื่อ problem2.cpp ในโปรเจค problem2 มีดังต่อไปนี้
Transform Transform::identity()
- คืน Transform ที่แทน identity transformation
Transform Transform::translate(float x, float y, float z)
- คืน Transform ที่แทนการเลื่อนแกนขนานไปตามแกน x เท่ากับอาร์กิวเมนต์ x, ไปตามแกน y เท่ากับอาร์กิวเมนต์ y, และไปตามแกน z เท่ากับอาร์กิวเมนต์ z
Transform Transform::translate_x(float x)
- คืน Transform ที่แทนการเลื่อนแกนขนานไปตามแกน x เท่ากับอาร์กิวเมนต์ x
Transform Transform::translate_y(float y)
- คืน Transform ที่แทนการเลื่อนแกนขนานไปตามแกน y เท่ากับอาร์กิวเมนต์ y
Transform Transform::translate_z(float z)
- คืน Transform ที่แทนการเลื่อนแกนขนานไปตามแกน z เท่ากับอาร์กิวเมนต์ z
Transform Transform::scale(float x, float y, float z)
- คืน Transform ที่แทนการย่อขยายขนาดตามแกน x เท่ากับอาร์กิวเมนต์ x เท่า, ตามแกน y เท่ากับอาร์กิวเมนต์ y เท่า, และตามแกน z เท่ากับอาร์กิวเมนต์ z เท่า
Transform Transform::scale_x(float x)
- คืน Transform ที่แทนการย่อขยายขนาดตามแกน x เท่ากับอาร์กิวเมนต์ x เท่า
Transform Transform::scale_y(float y)
- คืน Transform ที่แทนการย่อขยายขนาดตามแกน y เท่ากับอาร์กิวเมนต์ y เท่า
Transform Transform::scale_z(float z)
- คืน Transform ที่แทนการย่อขยายขนาดตามแกน z เท่ากับอาร์กิวเมนต์ z เท่า
Transform Transform::rotate(float degrees, Vector3 axis)
- คืน Transform ที่แทนการหมุนเป็นมุม degrees องศา รอบแกน axis
Transform Transform::rotate_x(float degrees)
- คืน Transform ที่แทนการหมุนเป็นมุม degrees องศา รอบแกน x
Transform Transform::rotate_y(float degrees)
- คืน Transform ที่แทนการหมุนเป็นมุม degrees องศา รอบแกน y
Transform Transform::rotate_z(float degrees)
- คืน Transform ที่แทนการหมุนเป็นมุม degrees องศา รอบแกน z
เมื่อเติมเสร็จเรียบร้อยแล้วให้ทดลอง compile ด้วยการกด F7
ถ้าคอมไพล์ผ่านให้เลือกโปรเจค problem2_test เป็น Startup Project โดยคลิกขวาที่ชื่อโปรเจค problem1_test แล้วเลือก Set as Startup Project หลังจากนั้นจึงลองรันมันด้วยการกด Ctrl+F5 ถ้าโปรแกรมคุณทำงานได้ถูกต้อง คุณจะผ่านการทดสอบที่เขียนไว้ใน problem2_test.cpp
หมายเหตุ: เพื่อให้ข้อนี้ไม่ง่ายเกินไป ผมได้ลบฟังก์ชัน inverse ออกไปจาก matrix4x4.h คุณจะต้องหาทางหา inverse ของวัตถุต่างๆ เอาเอง
ข้อ 3: Mesh
เมื่อทำข้อ 3 เสร็จแล้วให้เปิดโปรเจค problem3 และ problem3_test
ในข้อนี้เราจะทำการ implement คลาส Mesh คุณสามารถดูรายละเอียดของคลาสนี้ได้จาก สไลด์
คุณสามารถแ้ก้ไข declaration ของคลาส Mesh ได้ในไฟล์ problem3.h แต่คุณไม่ควรแก้ลบ method ที่มีอยู่แล้วออกไปเนื่องจากโค้ดส่วนอื่นใช้ method เหล่านี้ อนึ่ง คลาส Mesh ที่ให้มาไม่มีฟีลด์ใดๆ อยู่เลย ฉะนั้นคุณจะต้องเพิ่มฟีลด์ที่เหมาะสมเข้าไปเอง
หลังจากแก้ไข declaration แล้วให้ไป implement method ต่างๆ ในไฟล์ problem3.cpp อย่าลืมว่าถ้าคุณเพิ่ม method ใดขึ้นมาคุณจะต้อง implement method นั้นด้วย
เมื่อคุณ implement คลาด Mesh เรียบร้อยแล้วให้ลองคอมไพล์ แล้วจึงเซต problem3_test เป็น Startup Project แล้วรัน ถ้าโปรแกรมคุณทำงานได้ถูกต้อง คุณจะผ่านการทดสอบที่เขียนไว้ใน problem3_test.cpp
ข้อ 4: อ่านไฟล์ .obj
ในข้อสี่ คุณจะทำการอ่านไฟล์ .obj เข้าใส่ในคลาส Mesh ที่เขียนไว้ในข้อ 3 อันดับแรกขอให้เปิดโปรเจคชื่อ problem4 และ problem4_test
คุณจะต้อง implement ฟังก์ชัน <geshi lang="c"> Mesh *parse_obj_file(const char *file_name) </geshi> ที่อยู่ในไฟล์ problem4/problem4.cpp
ฟังก์ชันนี้จะรับ string file_name ซึ่งบรรจุชื่อไฟล์ .obj เข้าไป คุณจะต้องเปิดอ่านไฟล์นี้ อ่านข้อมูลในไฟล์ .obj แล้ว allocate instance ของคลาส Mesh ขึ้นมาใหม่เพื่อเก็บข้อมูลจากไฟล์ เมื่อเสร็จแล้วให้คืน instance ที่ allocate ขึ้นมาใหม่กลับไปให้ผู้เรียกฟังก์ชัน
คุณสามารถอ่านรูปแบบของไฟล์ .obj ได้ในเวบไซต์นี้: http://www.royriggs.com/obj.html
คำสั่งในไฟล์ .obj ที่คุณจะต้องสามารถประมวลผลได้อย่างถูกต้องคือ
- v (ใช้เพิ่มตำแหน่ง)
- vn (ใช้เพิ่ม normal)
- vt (ใช้เพิ่ม texture coordinate)
- f (ใช้เพิ่ม face)
ส่วนคำสั่งอื่นถ้าอ่านเจอให้ข้ามไป ไม่ต้องเขียนโค้ดเพื่อประมวลผลคำสั่งเหล่านั้น
เมื่อคุณ implement ฟังก์ชันเสร็จแล้ว ให้ลองคอมไพล์โค้ดดู ถ้าคอมไพล์ได้ถูกต้องคุณจะสามารถรันโปรแกรม problem4_test ได้ คุณสามารถทดสอบว่าโปรแกรมของคุณทำงานได้ถูกต้องหรือไม่ได้โดยการรันคำสั่งต่อไปนี้ในไดเรคทอรีที่คุณขยายไฟล์ homework-02.zip เอาไว้
C:\Directory ที่ขยาย homework-02.zip> Debug\problem4_test <ชื่อไฟล์ .obj>
หรือ
C:\Directory ที่ขยาย homework-02.zip> Release\problem4_test <ชื่อไฟล์ .obj>
โดยจะใช้คำสั่งแรกหรือคำสั่งที่สองขึ้นอยู่กับว่าขณะนั้นคุณคอมไพล์โปรแกรมแบบ Debug (มีข้อมูลเกี่ยวกับการดีบัก ทำให้โค้ดช้า) หรือ Release (ดีบักไม่ได้ แต่เร็ว)
ตัวอย่างผลลัพธ์ที่ถูกต้อง
Release\problem4_test data\bunny.obj | Release\problem4_test data\garg.obj | Release\problem4_test data\torus.obj |
คำแนะนำ
ในไฟล์ include\cglib\string.h จะมีฟังก์ชัน <geshi lang="C"> std::vector<std::string> split(const std::string &s, char delim); </geshi> ซึ่งคุณสามารถป้อนสตริง s และตัวอักษร delim แล้วมันจะคืน vector ของสตริงหลายตัวที่เิิกิดจากการตัด s เป็นท่อนๆ แต่ละท่อนขั้นด้วยตัวอักษร delim เช่นถ้าเรียก <geshi lang="C"> std::vector<std::string> parts = split("v 0.5 0.3 0.7", ' '); </geshi> คุณจะได้ว่า parts[0] = "v", parts[1] = "0.5", parts[2] = "0.3", และ parts[3] = "0.7"
ข้อ 5: แสดงผล MeshNode
หลังจากทำข้อ 4 เสร็จแล้ว ให้เปิดโปรเจค problem5 และ problem5_test
ในข้อ 5 คุณจะต้อง implement ฟังก์ชัน <geshi lang="C"> void render(MeshNode *node); </geshi> โดยฟังก์ชันนี้มีหน้าที่ใช้คำสั่ง OpenGL ในการแสดงผลคลาส MeshNode (ซึ่งแทน mesh อันหนึ่งพร้อมด้วย material ของ face แต่ละ face)
MeshNode
คลาส MeshNode มีส่วนประกอบอยู่สามส่วนคือ
- mesh ซึ่งเป็น instance ของคลาส Mesh ที่คุณเขียนไปในข้อ 3 โดยคุณสามารถเอา pointer ไปยัง mesh ได้โดยการใช้เมธอด Mesh *MeshNode::get_mesh()
- material_mapping ซึ่งเป็น instance ของคลาส MeshMaterialMapping (ดู include/cglib/mesh_material_mapping.h และ cglib/mesh_material_mapping.cpp) โดยคุณสามารถเอา pointer ไปยัง material_mapping ได้โดยการใช้เมธอด MeshMaterialMapping *MeshNode::get_material_mapping()
- material_list ซึ่งเป็น instance ของคลาส MaterialList (ดู include/cglib/material_list.h และ cglib/material_list.h) โดยคุณสามารถเอา pointer ไปยัง material_list ได้ด้วยการใช้เมธอด MeshMaterialMapping *MeshNode::get_material_list()
โดยที่ mesh, material_mapping, และ material_list มีความสัมพันธ์กันดังต่อไปนี้
- material ของ face ทั้งหมดจะถูกเก็บอยู่ใน material_list
- คุณสามารถหาได้ว่ามี material อยู่ทั้งหมดกี่ material โดยใช้เมธอด int MaterialList::get_material_count();
- material จะมีหมายเลขตั้งแต่ 0 ถึง get_material_count()-1
- คุณสามารถเอา pointer ไปยัง material หมายเลข j ได้ด้วยการใช้เมธอด Material *MaterialList::get_material(int material_index); โดยในกรณีต้องเรียก material_list->get_material(j);
- แต่ละ face ใน mesh จะมี material ของมันเอง
- หมายเลขของ material ของ face ต่างๆ จะถูกเก็บไว้ใน material_mapping
- คุณสามารถหาหมายเลขของ material ของ face หมายเลข k ได้ด้วยการใช้เมธอด int MeshMaterialMapping::get_face_material_index(int face_index); โดยในกรณีเราจะเรียก material_mapping->get_material_index(k);
การแสดงผล mesh
ในโค้ดที่ให้ไปมีโค้ดที่ใช้แสดงผล mesh อยู่แล้วใน problem4/problem4_test.cpp ฟังก์ชัน void MyApp::Draw()
<geshi lang="C++"> virtual void Draw() { FOR(face_index, mesh->get_face_count()) { glBegin(GL_POLYGON);
FOR(vertex_index, mesh->get_face_vertex_count(face_index)) { Point3 position = mesh->get_face_vertex_position(face_index, vertex_index); Vector3 normal = mesh->get_face_vertex_normal(face_index, vertex_index);
glNormal3f(normal.x, normal.y, normal.z); glVertex3f(position.x, position.y, position.z); }
glEnd(); } } </geshi>
ฟังก์ชัน Draw จะทำการวาด face ของ mesh ทีละ face ด้วยการใช้ GL_POLYGON
สิ่งที่แตกต่างกันระหว่างฟังก์ชัน Draw และ render คือในฟังก์ชัน Draw จะไม่มี material มาเกี่ยวข้อง แต่ปัญหาที่คุณจะต้องแก้ในการเขียนฟังก์ชัน render คือ ทำอย่างไรถึงใช้ข้อมูล material ของหน้าต่างๆ ที่เก็บอยู่ใน MeshNode ในการแสดงผล face ได้ด้วย
โด้ดที่คุณเขียนในฟังก์ชัน render ควรจะมีลักษณะคล้ายกับฟังก์ชัน Draw ข้างบน กล่าวคือ
- โค้ดของคุณจะต้องไล่แสดงผล face ไปทีละ face ด้วยการใช้ GL_POLYGON คล้ายๆ ข้างบน
- สำหรับ face แต่ละ face โค้ดของคุณจะต้องหาหมายเลขของ material ของ face นั้น พร้อมทั้ง pointer ไปยัง instance ของคลาส Material ที่แทน material ของ face นั้น
- โค้ดของคุณจะต้องเข้าใจข้อมูลที่เก็บอยู่ใน instance ของคลาส Material และใช้คำสั่ง OpenGL กับข้อมูลเหล่านั้นเพื่อแสดงผล material ต่างๆ ให้เหมาะสม
- โค้ดของคุณไม่ควรเรียกคำสั่งที่เปลี่ยนแปลง Modelview matrix หรือ lighting เลย มันควรจะเีรียกคำสั่งที่เกี่ยวข้องกับการวาดรูป และการปรับเปลี่ยน material เท่านั้น