บทความนี้เป็นการใช้ภาษาไพธอนของบอร์ด Raspberry Pi ใช้งานไลบรารี OpenGL เพื่อแสดงผลภาพแบบ 3 มิติเป็นกล่องสี่เหลี่ยมหมุนไปทางแกน X,Y และ Z
อุปกรณ์ที่ต้องใช้งาน
- บอร์ด Raspberry Pi 3 หรือ 4
OpenGL
OpenGL เป็นไลบรารีด้านกราฟิก 2 มิติและ 3 มิติแบบระบบเปิด ที่ทำงานได้กับหลากหลายแพลตฟอร์มและถูกพัฒนาอย่างต่อเนื่องมาตั้งแต่ ค.ศ. 1992 โดยไปป์ไลน์ (pipe-line) ของการทำงานของ OpenGL ประกอบด้วย 2 ส่วน คือ ส่วนของภาพที่ถูกนำมาใช้เป็นลายผิวหรือข้อมูลพิกเซล กับส่วนของข้อมูลพีชคณิตที่ใช้กำหนดตำแหน่งของพิกัดเวอร์เท็กซ์ พิกัดลายผิว และอื่น ๆ โดยตัวไลบรารีจะนำข้อมูลของเวอร์เท็กซ์มาคำนวณเพื่อสร้างภาพผลลัพธ์ไปเก็บไว้ในบัฟเฟอร์ สุดท้ายผู้เขียนโปรแกรมนำบัฟเฟอร์ผลลัพธ์ไปแสดงผลที่อุปกรณ์แสดงผลต่อไป
ไปป์ไลน์ (pipe-line) การสั่งงานไลบรารีมีด้วยกัน 2 ลักษณะซึ่ง OpenGL แต่ละรุ่นรองรับและสนับสนุนการทำงานแตกต่างกันไป แต่อย่างไรก็ดีใน OpenGL รุ่นหลังนั้นมีหลักการทำงานเป็นแบบการเขียนโปรแกรมเฉดเดอร์ (Shader Programming) เพื่อสั่งการทำงานของเฉดเดอร์แต่ละตัวทำให้ผู้เขียนสามารถสั่งงานได้ด้วยตนเอง ซึ่งแตกต่างจากไปป์ไลน์รุ่นก่อนหน้าที่เรียกว่าเป็นแบบจำกัดฟังก์ชัน (fixed function) ที่ผู้เขียนโปรแกรมสามารถใช้งานในความสามารถพื้นฐานของไลบรารีได้ แต่ถ้าต้องการใช้งานความสามารถพิเศษที่นอกเหนือขึ้นไปต้องเรียกใช้ไลบรารีเฉพาะที่การ์ดแสดงผลนั้นรองรับ จึงเป็นเรื่องท้าทายอย่างมากสำหรับนักเขียนโปรแกรมกราฟิกที่ต้องเขียนโปรแกรมให้รองรับกับการ์ดแสดงผลที่หลากหลายค่าย และแต่ละค่ายมีหลากหลายรุ่น
ตัวไลบรารี OpenGL มีการพัฒนาไปหลากหลายแพลตฟอร์มและมีชื่อเรียกที่แตกต่างกัน เช่น OpenGL ES เป็นไลบรารีที่ใช้กับแพลตฟอร์มของอุปกรณ์พกพา (รวมถึงบอร์ด Raspberry Pi ด้วย) และ WebGL ที่นำ OpenGL ES มาปรับปรุงเพื่อให้ใช้งานได้กับการแสดงผลผ่านเว็บบราวเซอร์ แต่ทั้ง OpenGL ES และ WebGL ล้วนทำงานตามหลักของไปป์ไลน์แบบใหม่ ดังนั้น ถ้ามีความเข้าใจกับแพลตฟอร์มหนึ่งจึงสามารถนำไปปรับปรุงไปใช้กับแพลตฟอร์มอื่น ๆ ได้ แต่อย่างไรก็ได้ในบทความนี้ไม่ได้ใช้ความสามารถดังกล่าวเนื่องจากเลือกใช้การสั่งงานแบบจำกัดฟังชันซึ่งทำงานได้กับ OpenGL ตั้งแต่รุ่น 1.1 ทำให้การ์ดแสดงผลส่วนใหญ่สามารถประมวลผลชุดคำสั่งได้
PyOpenGL เป็นไลบรารีของภาษาไพธอนที่รองรับการทำงานข้ามแพลตฟอร์มของภาษาไพธอน ทำให้สามารถเขียนโค้ดแล้วใช้งานกับ Windows, macOS, Linux, Raspberrypi ได้ นอกจากนี้การทำงานของ PyOpenGL รองรับการใช้งาน OpenGL รุ่น 1.1 ถึง 4.4, GLES รุ่น 1 ถึง 3.1, GLU, EGL, WGL, GLX, GLUT/FreeGLUT และ GLE3 ปัจจุบัน PyOpenGL เป็นรุ่น 3.1.5 (2021-06-29)
เนื่องจากไลบรารี OpenGL เป็นเพียงไลบรารีสำหรับกราฟิก 2 และ 3 มิติ โดยมี PyOpenGL เป็นตัวเชื่อมประสานระหว่างไลบรารี OpenGL กับภาษาไพธอน แต่การใช้งานสำหรับการแสดงผลจะต้องเรียกผ่านทางไลบรารีอื่น โดยในบทความนี้ใช้ไลบรารี PyGame เป็นตัวควบคุมการเชื่อมประสานระหว่างโปรแกรมที่เขียนกับผู้ใช้
ติดตั้งไลบรารี
การติดตั้งไลบรารี OpenGL เพื่อใช้งานกับบอร์ด Raspberry Pi บนระบบปฏิบัติการ Raspbian ต้องใช้คำสั่งติดตั้งดังนี้
sudo pip3 install pyopengl
กรณีที่ใช้ Ubuntu 20.10 ซึ่งไม่มี Python 3 ติดตั้งมาด้วย ให้ใช้คำสั่งต่อไปนี้
sudo pip install pyopengl
เมื่อติดตั้งไลบรารีพร้อมใช้งานสามารถทดสอบเพื่อความมั่นใจได้ด้วยการเข้าโปรแกรมภาษาไพธอนและเขียนคำสั่งต่อไปนี้
import OpenGL
print("PyOpenGL version {}".format(OpenGL.__version__))
จะได้ผลลัพธ์ดังภาพที่ 2
การใช้งาน OpenGL
การเขียนโปรแกรมเชื่อมประสานกับโอเพนจีแอลโดยใช้ PyGame แล้วให้ PyOpenGL ทำงานอยู่เหนือไลบรารี PyGame อีกชั้นหนึ่ง การเรียกใช้ไลบรารี PyOpenGL จะต้องเรียกใช้ดังนี้
โดยตัว OpenGL.GL เป็นไลบรารีสั่งงาน OpenGL และ OpenGL.GLU เป็นไลบรารีช่วยเหลือในการใช้งาน OpenGL
โปรแกรมต้นแบบ
โปรแกรมต้นแบบสำหรับการใช้ PyGame เป็นส่วนเชื่อมประสานกับผู้ใช้และ PyOpenGL เป็นตัวเชื่อมต่อกับการ์ดแสดงผลเพื่อดำเนินการประมวลผลกราฟิกเป็นดังโค้ด ex01.py
# -*- coding: UTF-8 -*-
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
def render():
pass
if __name__=="__main__":
pygame.init()
fpsClock = pygame.time.Clock()
pygame.display.set_mode((320,240), pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.set_caption("ex01")
glViewport(0,0,320,240)
gluOrtho2D(-1.0,1.0,1.0,-1.0)
running = True
while running:
fpsClock.tick(30)
glClearColor(1.0, 1.0, 1.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
render()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
pygame.quit()
จาก ex01.py จะพบว่าการ set_mode กำหนดให้ใช้แบบ Double Buffer เนื่องจาก OpenGL มีหน้าที่ประมวลผลกราฟิก เมื่อประมวลผลเสร็จจะเก็บผลลัพธ์ไว้ในหน่วยความจำ หน้าที่ของผู้เขียนโปรแกรมคือนำผลลัพธ์นั้นมาส่งให้ส่วนแสดงผล ดังนั้น วิธี double buffer หรือมีหน่วยเก็บข้อมูล 2 ส่วน คือ ส่วนแสดงผล กับหน่วยเก็บผลลัพธ์ ผู้เขียนโปรแกรมสั่ง display.flip() เพื่อนำข้อมูลผลลัพธ์คัดลอกลงส่วนแสดงผล หลังจากนั้นส่วนประมวลผลกราฟิกหรือ OpenGL จึงดำเนินการประมวลผลรอบถัดไปเพื่อนำผลลัพธ์มาเก็บในหน่วยความจำผลลัพธ์และทำวนซ้ำไปเรื่อย ๆ และการออกจากโปรแกรมทำได้ด้วยการคลิกที่ปุ่มปิดโปรแกรม ซึ่งผลลัพธ์ของการทำงานเป็นดังภาพที่ 3
หน้าจอแสดงผล
หน้าจอแสดงผลหรือมุมมอง (View port) เป็นการกำหนดช่วงค่าของการแสดงผลของโอเพนจีแอล ซึ่งไม่จำเป็นต้องเป็นค่าเดียวกับความละเอียดของจอแสดงผล เช่น จอแสดงผลมีความละเอียด 1368×768 จุด ซึ่งหมายความว่าค่าของจุดที่แสดงผลได้จะมีค่าเป็น (0,0) จนถึง (1367,767) ส่วนในกรณีที่ต้องการให้โอเพนจีแอลมีมุมมองเป็นพิกัดในช่วง (100,100) ถึง (199, 199) ดังนั้น จุดที่อยู่ตำแหน่ง (200,200) จะไม่ถูกนำมาแสดงผลเนื่องจากอยู่นอกเหนือมุมมองที่กำหนดไว้
ชุดคำสั่งสำหรับกำหนดวิวพอร์ตของโอเพนจีแอล คือ
โดยที่
- x คือ ค่าคอลัมน์ซ้ายสุดของส่วนแสดงผล (ปกตินิยมกำหนดเป็น 0)
- y คือ ค่าแถวบนสุดของส่วนแสดงผล (ปกตินิยมกำหนดเป็น 0)
- width คือ ความกว้างของส่วนแสดงผล
- height คือ ความสูงของส่วนแสดงผล
ดังนั้น ถ้าต้องการกำหนดวิวพอร์ตเป็นช่วง (100,100)-(299,299) คำสั่งที่ได้ คือ
นอกจากการกำหนดวิวพอร์ตแล้ว ผู้เขียนโปรแกรมต้องกำหนดลักษณะของปริภูมิที่ใช้งาน คือ ปริภูมิ 2 มิติ และ 3 มิติ ซึ่งมีผลต่อรูปทรงที่นำมาแสดงผลแตกต่างกัน
การแสดงผลด้วยปริภูมิ 2 มิติ
การแสดงผลของปริภูมิ 2 มิติอาศัยการอ้างอิงตำแหน่งของจุดของรูปทรง หรือวัตถุโดยใช้ตำแหน่งของวัตถุบนแกน X กับแกน Y ซึ่งการกำหนดช่วงค่าของค่าทางซ้าย (left หรือค่าน้อยสุดของแกน x), ขวา (right หรือค่ามากสุดของแกน x), ล่าง (bottom หรือค่าน้อยสุดของแกน y) และด้านบน (top หรือค่ามากสุดของแกน y) ดังภาพที่ 4 และชุดคำสั่งในโอเพนจีแอลต้องใช้คำสั่งดังรูปแบบต่อไปนี้
ถ้าต้องการให้ค่าทางซ้ายมือเป็น -10 ค่าทางขวามือเป็น 10 ค่าด้านล่างเป็น -10 และค่าด้านบนเป็น 10 ต้องสั่งคำสั่งดังนี้
gluOrtho2D(-10, 10, -10, 10)
ถ้าต้องการให้ค่าทางซ้ายมือเป็น 0 ค่าทางขวามือเป็น 100 ค่าด้านล่างเป็น 0 และค่าด้านบนเป็น 100 ต้องสั่งคำสั่งดังนี้
gluOrtho2D(0,100,0,100)
ความแตกต่างระหว่างคำสั่ง gluOrthor2D กับ glViewport เป็นดังภาพที่ 5 และในการแสดงผลต้องแปลงจากปริภูมิ 2 มิติให้เป็นวิวพอร์ตดังภาพที่ 6
การแสดงผลด้วยปริภูมิ 3 มิติ
ปริภูมิ 3 มิติอ้างอิงพิกัดของจุดของรูปทรงหรือวัตถุโดยใช้ค่าตำแหน่งในแกน X ค่าตำแหน่งในแกน Y และค่าตำแหน่งในแกน Z เป็นค่าอ้างอิง ดังภาพที่ 7 ดังนั้น เมื่อนำข้อมูลในปริภูมิ 3 มิติมาแสดงผลในจอแสดงผลที่เป็น 2 มิติ จึงต้องทำการแปลงพิกัดของจุดต่างๆ และเลือกจุดที่อยู่ด้านหน้าสุด (ในโอเพนจีแอลเรียกวิธีการเลือกนี้ว่า z-test) เพื่อใช้เป็นค่าของเม็ดสีที่มองเห็น ซึ่งวิธีการแปลงเป็นดังภาพที่ 8
จากภาพที่ 8 จะพบว่าจุด P(x,y,z) เป็นจุดในปริภูมิ 3 มิติ เมื่อทำการแปลงจากปริภูมิ 3 มิติเป็นปริภูมิ 2 มิติ หรือการโปรเจ็กชั่น (Projection) มาเป็นจุด P’(x, y) โดยมีจุดกึ่งกลางอยู่ที่ (0,0,0) มีค่า f เป็นระยะห่างระหว่างฉากรับ (จอแสดงผล) กับจุดกึ่งกลาง ซึ่งวิธีการคำนวณตามภาพ 8 มีความซับซ้อน จึงเปลี่ยนการวางฉากรับให้อยู่เข้ามาในแกน Z ดังภาพที่ 9
อัตราส่วน (Ratio) ของสามเหลี่ยมมุมฉากที่เกิดจากจุด P(x,y,z) ที่กระทำกับแกน Z และจากจุด P’(x’, y’) กับแกน Z เป็น อัตราส่วนของสามเหลี่ยมมุมฉากที่เท่ากัน และกำหนดให้ค่า Z จากภาพที่ 8 เป็น z-Far ดังในภาพที่ 9 และค่า f จากภาพที่ 8 เป็นค่า z-Near ในภาพที่ 9
ชุดคำสั่งสำหรับกำหนดปริภูมิ 3 มิติ มีหลักการเหมือนกับการมองจากกล้อง คือ กำหนดมุมองศารับภาพ (FOV: Field of View) อัตราส่วนของฉากรับ ระยะใกล้สุด และระยะไกลสุดดังรูปแบบคำสั่งต่อไปนี้
โดยที่
fovy คือ องศารับภาพของแกน Y โดยค่า fovy มีความสัมพันธ์กับค่า f ดังสมการต่อไปนี้
aspect คือ อัตราส่วนของฉากรับ (ความกว้างต่อความสูง)
zNear คือ ระยะแกน Z ที่ใกล้ที่สุด (ต้องเป็นค่าบวก)
zFar คือ ระยะแกน Z ที่ไกลที่สุด (ต้องเป็นค่าบวก)
การวาดรูปทรง
การวาดรูปทรงของวัตถุด้วยโอเพนจีแอลมีหลักการทำงานดังขั้นตอนต่อไปนี้
1. เลือกวิธีของการวาดเพื่อสร้างรูปทรงดังภาพที่ 10
2. กำหนดพิกัดของเวอร์เท็กซ์ตามลักษณะของการวาด
ชุดคำสั่งสำหรับการวาดรูปทรงเป็นดังนี้
โดยที่ วิธีการวาด คือ ชื่อค่าคงที่ของการวาดซึ่งค่าเป็นค่าใดดังต่อไปนี้
- GL_POINTS
- GL_LINES
- GL_LINE_STRIP
- GL_LINE_LOOP
- GL_TRIANGLES
- GL_TRIANGLE_STRIP
- GL_TRIANGLE_FAN
ชุดคำสั่งสำหรับกำหนดค่าของเวอร์เท็กซ์เป็นดังนี้
โดยที่
- x คือ ค่าพิกัดของเวอร์เท็กซ์ในแกน X
- y คือ ค่าพิกัดของเวอร์เท็กซ์ในแกน Y
- z คือ ค่าพิกัดของเวอร์เท็กซ์ในแกน Z
กรณีต้องการวาดสามเหลี่ยม ดังภาพที่ 11 สามารถเขียนโค้ดได้ดังนี้
กรณีต้องการวาดสี่เหลี่ยม ซึ่งต้องอาศัยการวาดสามเหลี่ยมสองรูปประกบกันดังภาพที่ 4.3 สามารถเขียนโค้ดได้ดังนี้
คำสั่งที่ทำหน้าที่สั่งให้ไดรเวอร์ดำเนินการส่งข้อมูลไปยังการ์ดแสดงผลเพื่อป้องการตกค้างของคำสั่งในบัฟเฟอร์ของไดรเวอร์เป็นดังนี้
glFlush( )
สำหรับกรณีที่ต้องการปรับให้จุดมีขนาดที่แตกต่างกัน (ปกติตั้งค่าไว้เป็น 1.0) แต่ต้องมีค่ามากกว่า 0.0 สามารถเรียกใช้คำสั่งกำหนดขนาดของจุดดังรูปแบบการใช้งานตอไปนี้
glPointSize( ขนาดของจุดที่ต้องการ )
คำสั่งสำหรับกำหนดขนาดความกว้างของเส้นมีรูปแบบการใช้งานดังนี้
glLineWidth ( ขนาดความกว้างของเส้นที่ต้องการ )
นอกจากวาดเส้นตรงเป็นเส้นต่อเนื่อง ผู้เขียนโปรแกรมสามารถวาดเส้นตรงที่มีลวดลายได้ตามความต้องการโดยเปิดใช้การทำงานของ GL_LINE_STIPPLE (และปิดเมื่อเลิกใช้) พร้อมกับกำหนดรูปแบบของลวดลายจากคำสั่งต่อไปนี้
glLineStipple( factor, pattern )
โดย
- pattern คือ ค่าตัวเลขขนาด 32 บิตที่ใช้แทนรูปแบบของลวดลายที่วาด ซึ่งแทนค่าบิต 1 เป็นการลงสี (on) และบิต 0 เป็นจุดว่าง (off)
- factor คือ จำนวนเท่าของแต่ละบิต เช่น กำหนดให้ factor มีค่าเป็น 2 เมื่อพบบิต 0 ในค่า pattern ตัวโอเพนจีแอลจะลงเป็นจุดว่าง 2 จุด แต่ถ้ากำหนดเป็น 4 จะมีความหมายเป็นการลงจุดว่าง 4 จุด
การเปิดและปิดการทำงานของเส้นกำหนดลวดลาย คือ
glEnable( GL_LINE_STIPPLE )
glDisable( GL_LINE_STIPPLE )
การกำหนดสี
สีที่ใช้ในโอเพนจีแอลเป็นเวกเตอร์ของสีแดง สีเขียว และสีน้ำเงิน อ้างอิงโดย (R, G, B) และแต่ละสีมีค่าเป็นตัวเลขทศนิยมจาก 0 ถึง 1 ในการบอกระดับความเข้มของแต่ละสี เมื่อมีการผสมสีสามารถสร้างสีได้สูงสุดถึง 16 ล้านสี การระบุค่าสีของโอเพนจีแอลใช้หลักการระบุความเข้มของสี 2 แบบ คือ ระบุเป็นค่าตัวเลขจำนวนเต็มที่มีค่าอยู่ในช่วงตั้งแต่ 0 ถึง 255 และระบุเป็นค่าทศนิยมจาก 0.0 ถึง 1.0 ดังภาพที่ 13
ชุดคำสั่งระบุค่าสีในโอเพนจีแอลแบบจำนวนเต็มเป็นดังนี้
glColor3b(r, g, b)
glColor4b(r, g, b, a)
โดยที่
r คือ ค่าความเข้มของสีแดง มีค่าตั้งแต่ 0 ถึง 255
g คือ ค่าความเข้มของสีเขียว มีค่าตั้งแต่ 0 ถึง 255
b คือ ค่าความเข้มของสีน้ำเงิน มีค่าตั้งแต่ 0 ถึง 255
a คือ ค่าความเข้มของสีอัลฟา มีค่าตั้งแต่ 0 ถึง 255
ชุดคำสั่งระบุค่าสีแบบค่าทศนิยมเป็นดังนี้
glColor3f(r, g, b)
glColor4f(r, g, b, a)
โดยที่
r คือ ค่าความเข้มของสีแดง มีค่าตั้งแต่ 0.0f ถึง 1.0f
g คือ ค่าความเข้มของสีเขียว มีค่าตั้งแต่ 0.0f ถึง 1.0f
b คือ ค่าความเข้มของสีน้ำเงิน มีค่าตั้งแต่ 0.0f ถึง 1.0f
a คือ ค่าความเข้มของสีอับฟา มีค่าตั้งแต่ 0.0f ถึง 1.0f
การกำหนดค่าสีของโพลีกอนสามารถทำได้โดยกำหนดค่าสีก่อนเข้าสู่การวาด glBegin/glEnd เช่น ต้องการวาดสามสี่เหลี่ยมสีเขียวสามารถเขียนโค้ดได้ดังนี้
glColor3f( 0.0f, 1.0f, 0.0)
glBegin(GL_TRIANGLES)
glVertex2f(… , … )
glVertex2f(… , … )
glVertex2f(… , … )
glVertex2f(… , … )
glVertex2f(… , … )
glVertex2f(… , … )
glEnd()
การกำหนดค่าสีของแต่ละเวอร์เท็กซ์สามารถเขียนโค้ดได้ดังนี้
glBegin(GL_TRIANGLES)
glColor3f( …, …, …)
glVertex2f(… , … )
glColor3f( …, …, …)
glVertex2f(… , … )
glColor3f( …, …, …)
glVertex2f(… , … )
glEnd()
กรณีที่ต้องการเปลี่ยนสีฉากหลังของจอแสดงผลจะต้องใช้คำสั่งล้างหน้าจอด้วยสีที่ต้องการ
ดังรูปแบบต่อไปนี้
glClearColor( r, g, b, a )
นอกจากต้องสั่งกำหนดสีฉากหลังแล้ว ผู้เขียนโปรแกรมจำเป็นต้องสั่งให้โอเพนจีแอลล้างค่าบัฟเฟอร์แสดงผลที่การ์ดแสดงผล โดยคำสั่งดังกล่าวมีรูปแบบการใช้งานดังนี้
glClear( แฟล็กการล้างค่าบัฟเฟอร์ )
โดย แฟล็กการล้างค่าบัฟเฟอร์เป็นชื่อบิตที่ใช้เป็นตัวแทนของประเภทบัฟเฟอร์ที่ต้องการล้างค่า ซึ่งได้แก่
- GL_COLOR_BUFFER_BIT สำหรับล้างค่าบัฟเฟอร์สีGL_DEPTH_BUFFER_BIT สำหรับล้างค่าบัฟเฟอร์ความลึก
- GL_ACCUM_BUFFER_BIT สำหรับล้างค่าบัฟเฟอร์ Accumulation
- GL_STENCIL_BUFFER_BIT สำหรับล้างค่าบัฟเฟอร์สเตนซิล
ดังนั้น เมื่อต้องการล้างบัฟเฟอร์ของสีและความลึก จะต้องสั่งงานดังนี้
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT )
หรือเขียนแยกเป็น 2 คำสั่งดังนี้
glClear( GL_COLOR_BUFFER_BIT )
glClear( GL_DEPTH_BUFFER_BIT )
การกำหนดค่าเริ่มต้นของค่าความลึกในบัฟเฟอร์ความลึกกำหนดโดยใช้คำสั่งดังนี้
glClearDepth( ค่าความลึกที่ต้องการ )
ตัวอย่างโปรแกรม ex02.py เป็นการวาดเส้น 3 เส้นโดยแต่ละเส้นมีค่าสีตามจุดยอดที่เป็นจุดเริ่มและจุดสิ้นสุดทำให้ได้ผลลัพธ์ออกมาตามภาพที่ 14
# -*- coding: UTF-8 -*-
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
def render():
glBegin(GL_LINES)
# Line No.1
glColor3f(1.0,0.0,0.0)
glVertex2f(-0.5, -0.5)
glColor3f(0.0,1.0,0.0)
glVertex2f( 0.0, 0.5)
# Line No.2
glColor3f(0.0,1.0,0.0)
glVertex2f( 0.0, 0.5)
glColor3f(0.0,0.0,1.0)
glVertex2f( 0.5, -0.5)
# Line No.3
glColor3f(0.0,0.0,1.0)
glVertex2f( 0.5, -0.5)
glColor3f(1.0,0.0,0.0)
glVertex2f(-0.5, -0.5)
glEnd()
if __name__=="__main__":
pygame.init()
fpsClock = pygame.time.Clock()
pygame.display.set_mode((320,240), pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.set_caption("ex02")
glViewport(0,0,320,240)
gluOrtho2D(-1.0,1.0,1.0,-1.0)
running = True
while running:
fpsClock.tick(30)
glClearColor(1.0, 1.0, 1.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
render()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
pygame.quit()
ตัวอย่างโปรแกรม ex03.py เป็นการวาดสามเหลี่ยมโดยการวาดด้วย GL_TRIANGLES ทำให้กำหนดข้อมูลของเวอร์เท็กซ์เพียง 3 ชุด จึงใช้หน่วยความจำของการ์ดแสดงผลน้อยกว่าและส่งข้อมูลเวอร์เท็กซ์จากคอมพิวเตอร์ไปยังการ์ดแสดงผลได้รวดเร็วกว่าการวาดด้วยเส้นตรงในต้วอย่างโปรแกรม ex02.py นอกจากนี้ เมื่อผู้ใช้กดแป้นพิมพ์หมายเลข 1 โปรแกรมจะแสดงเพียงจุดเวอร์เท็กซ์ของสามเหลี่ยม กดแป้นพิมพ์หมายเลข 2 โปรแกรมจะแสดงเส้นของสามเหลี่ยม และกดแป้นพิมพ์หมายเลข 3 โปรแกรมจะแสดงสามเหลี่ยมแบบเติมสีในรูปสามเหลี่ยมดังภาพที่ 1 ภาพซ้าย กลางและขวา
# -*- coding: UTF-8 -*-
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
def draw_tri():
glBegin(GL_TRIANGLES)
glColor3f(1.0,0.0,0.0)
glVertex2f(-0.5, -0.5)
glColor3f(0.0,1.0,0.0)
glVertex2f( 0.0, 0.5)
glColor3f(0.0,0.0,1.0)
glVertex2f( 0.5, -0.5)
glEnd()
pygame.init()
fpsClock = pygame.time.Clock()
pygame.display.set_mode((800,600), pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.set_caption("Ex03")
glViewport(0,0,800,600)
gluOrtho2D(-1.0,1.0,1.0,-1.0)
running = True
glLineWidth(4)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
while running:
fpsClock.tick(30)
glClearColor(1.0, 1.0, 1.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
draw_tri()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_1:
glPointSize(5)
glPolygonMode(GL_FRONT_AND_BACK, GL_POINT)
if event.key == pygame.K_2:
glLineWidth(4)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
if event.key == pygame.K_3:
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
pygame.display.flip()
pygame.quit()
จากตัวอย่าง ex03.py จะพบว่าคำสั่งสำหรับกำหนดโหมดการวาดโพลีกอนมี 3 รูปแบบ คือ โหมดการวาดเฉพาะเวอร์เท็กซ์ของโพลีกอน (GL_POINT) โหมดการวาดเส้นเชื่อมต่อระหว่างเวอร์เท็กซ์ของโพลีกอน (GL_LINE) และโหมดการวาดโพลีกอนแบบเติมสีภายในโพลีกอน (GL_FILL) ซึ่งรูปแบบของคำสั่งสำหรับกำหนดโหมดการวาดเป็นดังนี้
glPolygonMode( ด้านของโพลีกอนที่ต้องการกำหนด, ค่าโหมดการวาด )
โดยค่าด้านของโพลีกอนที่ต้องการกำหนดโหมดของการวาดนั้นมี 3 ค่า คือ การเลือกให้มีผลเฉพาะด้านหน้าของโพลีกอน (GL_FRONT) การเลือกให้มีผลเฉพาะด้านหลังของโพลีกอน (GL_BACK) และให้มีผลทั้งด้านหน้าและด้านหลังของโพลีกอน (GL_FONT_AND_BACK)
ตัวอย่างโปรแกรม ex04.py เป็นการวาดสี่เหลี่ยมโดยใช้สามเหลี่ยมสองรูปประกบกัน โดยแยกข้อมูลของเวอร์เท็กซ์เก็บในตัวแปรแทนการกำหนดค่าใน glVertex2f() ทำให้สะดวกในการกำหนดค่าข้อมูล และไม่ต้องแก้ไขโปรแกรมย่อยสำหรับวาดโพลีกอน ซึ่งผลลัพธ์ของโปรแกรมเป็นดังภาพที่ 15
# -*- coding: UTF-8 -*-
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
vertices = ((-0.5, 0.5),
(0.5,0.5),
(0.5,-0.5),
(-0.5,-0.5))
colors = ((1.0,0.0,0.0),
(0.0,1.0,0.0),
(0.0,0.0,1.0),
(1.0,1.0,1.0))
rect = ((0,1,2),(2,3,0))
def draw(obj):
glBegin(GL_TRIANGLES)
for edge in obj:
for v in edge:
glColor3fv(colors[v])
glVertex2fv(vertices[v])
glEnd()
pygame.init()
fpsClock = pygame.time.Clock()
pygame.display.set_mode((800,600), pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.set_caption("Ex04")
glViewport(0,0,800,600)
gluOrtho2D(-1.0,1.0,1.0,-1.0)
running = True
while running:
fpsClock.tick(30)
glClearColor(1.0, 1.0, 1.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
draw(rect)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
pygame.quit()
นอกจากนี้โอเพนจีแอลมีคำสั่งสำหรับกำหนดให้ผู้เขียนโปรแกรมเลือกให้มีการคำนวณค่าเฉพาะด้านที่ต้องการเพื่อแสดงผล เช่น เลือกให้คำนวณเฉพาะด้านหน้าของพื้นผิว ซึ่งจะทำให้ความเร็วในการทำงานสูงขึ้น แต่เมื่อใดที่ด้านหลังของพื้นผิวนั้นหันหน้าเข้าด้านที่ผู้ใช้มองจะไม่ถูกนำมาคำนวณอันส่งผลให้ไม่สามารถมองเห็นด้านหลังของพื้นผิวนั้นได้ ซึ่งรูปแบบของคำสั่งเป็นดังนี้
glCullFace( mode )
โดยที่ mode มีด้วยกัน 3 ค่า คือ
- GL_FRONT,
- GL_BACK
- GL_FRONT_AND_BACK
คำสั่งสำหรับเปิดหรือปิดการทำงานของคำสั่ง glCullFace( ) มีรูปแบบดังนี้
glEnable( GL_CULL_FACE )
glDisable( GL_CULL_FACE )
คำสั่งสำหรับตัดสินว่าด้านใดเป็นด้านหน้ากระทำโดยระบุลำดับการอ้างอิงตำแหน่งของเวอร์เท็กซ์ว่าหมุนตามเข็มนาฬิกา (GL_CW) หรือทวนเข็มนาฬิกา (GL_CCW) แก่คำสั่งตามรูปแบบต่อไปนี้ ซึ่งค่าโดยปกติเป็น GL_CCW
glFrontFace( ลำดับการอ้างอิงเวอร์เท็กซ์ )
ตัวอย่างโปรแกรม ex05.py เป็นการวาดกล่องสี่เหลี่ยมในปริภูมิ 3 มิติ โดยกำหนดให้กล่องสี่เหลี่ยมหมุนครั้งละ 1 องศาในแกน x y และ z ดังตัวอย่างในภาพที่ 16
# -*- coding: UTF-8 -*-
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
vertices = ((-1, 1, -1),
( 1, 1, -1),
( 1, -1, -1),
(-1, -1, -1),
(-1, 1, 1),
( 1, 1, 1),
( 1, -1, 1),
(-1, -1, 1))
colors = ( (1.0,0.0,0.0),
(0.0,1.0,0.0),
(0.0,0.0,1.0),
(1.0,1.0,1.0),
(1.0,0.0,0.0),
(0.0,1.0,0.0),
(0.0,0.0,1.0),
(1.0,1.0,1.0))
cube = ( (0,1,2), #front
(2,3,0),
(1,0,4), # top
(4,5,1),
(3,2,6), # bottom
(6,7,3),
(4,0,3), # Left
(3,7,4),
(1,5,6), # right
(6,2,1),
(5,4,7), # back
(7,6,5))
def draw3D(obj):
glBegin(GL_TRIANGLES)
for edge in obj:
for v in edge:
glColor3fv(colors[v])
glVertex3fv(vertices[v])
glEnd()
pygame.init()
fpsClock = pygame.time.Clock()
ratio = 8.0/6.0
pygame.display.set_mode((800,600), pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.set_caption("Ex4.4")
glViewport(0,0,800,600)
running = True
gluPerspective(30.0, ratio, 0.1, 50.0)
glTranslatef(0.0, 0.0, -10.0)
glClearColor(0.0, 0.0, 0.0, 1.0)
glEnable(GL_DEPTH_TEST)
while running:
fpsClock.tick(30)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
glRotatef( 1, 1, 1, 1)
draw3D(cube)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
pygame.quit()
การแสดงข้อความ
การแสดงข้อความด้วยโอเพนจีแอลนิยมกระทำกัน 2 วิธี คือ วาดด้วยการสร้างลายผิวของข้อความ และวาดด้วยภาพบิตแมพ โดยในบทนี้กล่าวถึงวิธีการที่ 2 ซึ่งมีหลักการทำงานดังนี้
- สร้างตัวแปรฟอนต์
- เรนเดอร์ข้อความเก็บในหน่วยความจำแบบพื้นผิว
- แปลงหน่วยความจำพื้นผิวให้เป็นข้อมูลบัฟเฟอร์แบบไบต์ที่เรียงข้อมูลแบบ RGBA
- กำหนดตำแหน่งที่ต้องการแสดงข้อความ
- วาดพิกเซลของบัฟเฟอร์ที่ได้จากขั้นตอนที่ 3
รูปแบบคำสั่งสำหรับแปลงข้อมูลพื้นผิวให้เป็นบัฟเฟอร์แบบไบต์ที่เรียงข้อมูลเป็นแบบ RGBA เป็นดังนี้
บัฟเฟอร์=pygame.image.tostring(พื้นผิว, “RGBA”, True )
คำสั่งระบุตำแหน่งแสดงข้อความของโอเพนจีแอลมีรูปแบบของคำสั่งดังนี้
glRasterPos2d(*(x, y))
คำสั่งสำหรับนำบัฟเฟอร์ที่แปลงให้เป็นรูปแบบ RGBA เพื่อแสดงผลบนจอแสดงผล ณ ตำแหน่งที่กำหนดจากคำสั่ง glRasterPos2d( ) มีรูปแบบดังนี้
glDrawPixels( ความกว้างของบิตแมพ, ความสูงของบิตแมพ, GL_RGBA, GL_UNSIGNED_BYTE, ตัวแปรบัฟเฟอร์ )
ตัวอย่างโปรแกรมที่ 6 ex06.py เป็นตัวอย่างแสดงคำว่า “Hello, World!!! สวัสดีชาวโลก” ด้วยฟอนต์ Tahoma ขนาด 20px สีเหลือง ณ ตำแหน่ง (-0.5, 0.0) โดยขั้นตอนทั้ง 5 ดังที่กล่าวมาก่อนหน้านี้ อยู่ในฟังก์ชันที่ชื่อ draw_text( )
# -*- coding: UTF-8 -*-
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
def draw_text(pos,text,color=(255,255,255,255),
font_name="Tahoma",size=20):
font = pygame.font.SysFont (font_name, size)
text_face = font.render(text, True, color,(0,0,0,255))
text_buffer = pygame.image.tostring(text_face, "RGBA", True)
glRasterPos2d(*pos)
glDrawPixels(text_face.get_width(), text_face.get_height(),
GL_RGBA, GL_UNSIGNED_BYTE, text_buffer)
pygame.init()
fpsClock = pygame.time.Clock()
pygame.display.set_mode((800,600), pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.set_caption("Ex06)
glViewport(0,0,800,600)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluOrtho2D(-1.0,1.0,1.0,-1.0)
running = True
while running:
fpsClock.tick(30)
glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
draw_text((-0.5, 0.0), "Hello, World!! สวัสดีชาวโลก",(255,255,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
pygame.quit()
การวาดด้วยลิสต์
จากตัวอย่างโปรแกรมที่ผ่านมาจะพบว่า ผู้เขียนโปรแกรมต้องส่งรายการของเวอร์เท็กซ์ไปให้การ์ดแสดงผลทุกครั้งที่มีการวาดวัตถุแต่ละภาพ ถ้าวัตถุมีจำนวนเวอร์เท็กซ์เยอะ หรือมีจำนวนวัตถุหลายชิ้นที่ต้องสั่งวาดอาจจะเกิดปัญหาคอขวดในการส่งข้อมูลจากโปรแกรมไปยังการ์ดแสดงผลได้ ด้วยเหตุนี้โอเพนจีแอลจึงมีวิธีการวาดจากหน่วยความจำภายในการ์ดแสดงผลโดยผู้เขียนโปรแกรมส่งรายการเวอร์เท็กซ์/นอร์มัล/สี/พิกัดลายผิวทั้งหมดไปเก็บในหน่วยความจำการ์ดแสดงผลและสั่งวาดเมื่อต้องการ
การจัดเก็บข้อมูลในหน่วยความจำของการ์ดแสดงผลอยู่ในรูปแบบของลิสค์ และสั่งยกเลิกการจองหน่วยความจำนี้หลังจากไม่ใช้งาน ดังนั้น ขั้นตอนของการวาดด้วยลิสต์จึงเป็นดังนี้
- ขอจองหน่วยความจำลิสต์จากโอเพนจีแอล
- สั่งสร้างลิสต์ใหม่ในหน่วยความจำการ์ดแสดงผล
- ตั้งค่าเหมือนการสั่งวาด
- สั่งสิ้นสุดการสร้างลิสต์
คำสั่งสำหรับขอจองหน่วยความจำลิสต์จะคืนค่าเป็นหมายเลขอ้างอิงในการติดต่อกับการ์ดแสดงผล และถ้าการขอจองถูกปฏิเสธจะได้ค่าคืนกลับเป็นค่าลบ โดยรูปแบบของคำสั่งเป็นดังนี้
หมายเลขอ้างอิงลิสต์ = glGenList( 1 )
คำสั่งสำหรับสร้างลิสต์ใหม่ในหน่วยความจำการ์ดแสดงผลของโอเพนจีแอลมีรูปแบบของการใช้งานดังนี้
glNewList( หมายเลขอ้างอิงลิสต์, GL_COMPILE )
คำสั่งสิ้นสุดการสร้างลิสต์มีรูปแบบการใช้งานดังนี้
glEndList()
เมื่อต้องการวาดวัตถุต้องใช้คำสั่งสำหรับวาดดังนี้
glCallList( หมายเลขอ้างอิงลิสต์ )
กรณีที่ต้องการยกเลิกการจองหน่วยความจำลิสต์ออกจากหน่วยความจำการ์ดแสดงผลต้องใช้คำสั่งตามรูปแบบต่อไปนี้
glDeleteLists( หมายเลขอ้างอิงลิสต์, 1 )
ตัวอย่างโปรแกรม ex07.py เป็นการแปลงโปรแกรมตัวอย่างที่ 4.4 ให้ทำงานในแบบลิสต์ จะพบว่า ในการวนรอบการทำงานจะไม่มีการส่งรายการเวอร์เท็กซ์ไปยังการ์ดแสดงผลเหลือเพียงแจ้งการ์ดแสดงผลให้เรียกใช้ลิสต์ที่ได้ขอจองและสร้างรายการคำสั่งเอาไว้ ทำให้การวาดไม่เสียเวลา ในการส่งข้อมูลไปที่การ์ดแสดงผล แต่มีผลทำให้การเริ่มต้นทำงานใช้เวลามากขึ้นเนื่องจาก
ต้องดำเนินการส่งชุดคำสั่งไปให้การ์ดแสดงผล
# -*- coding: UTF-8 -*-
import pygame
import sys
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
vertices = ((-1, 1, -1),
( 1, 1, -1),
( 1, -1, -1),
(-1, -1, -1),
(-1, 1, 1),
( 1, 1, 1),
( 1, -1, 1),
(-1, -1, 1))
colors = ( (1.0,0.0,0.0),
(0.0,1.0,0.0),
(0.0,0.0,1.0),
(1.0,1.0,1.0),
(1.0,0.0,0.0),
(0.0,1.0,0.0),
(0.0,0.0,1.0),
(1.0,1.0,1.0))
cube = ( (0,1,2), #front
(2,3,0),
(1,0,4), # top
(4,5,1),
(3,2,6), # bottom
(6,7,3),
(4,0,3), # Left
(3,7,4),
(1,5,6), # right
(6,2,1),
(5,4,7), # back
(7,6,5))
pygame.init()
fpsClock = pygame.time.Clock()
ratio = 8.0/6.0
pygame.display.set_mode((800,600), pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.set_caption("Ex07")
glViewport(0,0,800,600)
running = True
glMatrixMode( GL_PROJECTION )
glLoadIdentity()
gluPerspective(30.0, ratio, 0.1, 50.0)
glTranslatef(0.0, 0.0, -10.0)
glClearColor(0.0, 0.0, 0.0, 1.0)
cube_id = glGenLists(1)
if cube_id < 0:
pygame.quit()
sys.exit()
glNewList( cube_id, GL_COMPILE )
glEnable(GL_DEPTH_TEST)
glBegin(GL_TRIANGLES)
for edge in cube:
for v in edge:
glColor3fv(colors[v])
glVertex3fv(vertices[v])
glEnd()
glEndList()
while running:
fpsClock.tick(30)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
glRotatef( 1, 1, 1, 1)
glCallLists( cube_id )
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
glDeleteLists( cube_id, 1 )
pygame.quit()
สรุป
จากบทความนี้จะพบว่าภาษาไพธอนสามารถใช้งานได้หลากหลายรวมถึงการเขียนโปรแกรมเพื่อแสดงกราฟิกแบบ 2 มิติ และ 3 มิติด้วยไลบรารี OpenGL ผ่านทาง PyGame และ PyOpenGL โดยในบทความนี้ได้แนะนำพื้นฐานการเขียนโปรแกรมเปิดหน้าต่าง การวาด และการตอบสนองกับผู้ใช้ ซึ่งเป็นพื้นฐานสำหรับการศึกษาการเขียนในระดับถัดไป และสุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ
แหล่งอ้างอิง
(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-06-29, 2021-06-30, 2021-10-11, 2024-04-04