[TH] PyOpenGL

บทความนี้เป็นการใช้ภาษาไพธอนของบอร์ด Raspberry Pi ใช้งานไลบรารี OpenGL เพื่อแสดงผลภาพแบบ 3 มิติเป็นกล่องสี่เหลี่ยมหมุนไปทางแกน X,Y และ Z

ภาพที่ 1 ผลลัพธ์จากโปรแกรม ex03.py

อุปกรณ์ที่ต้องใช้งาน

  1. บอร์ด 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

ภาพที่ 2 ผลลัพธ์จากการนำเข้าไลบรารีและสอบถามรุ่นของ PyOpenGL

การใช้งาน 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

ภาพที่ 3 ผลลัพธ์จากโปรแกรมต้นแบบ ex01.py

หน้าจอแสดงผล

หน้าจอแสดงผลหรือมุมมอง (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 และชุดคำสั่งในโอเพนจีแอลต้องใช้คำสั่งดังรูปแบบต่อไปนี้

ภาพที่ 4 การอ้างอิงพิกัดในปริภูมิ 2 มิติ ของคำสั่ง gluOrtho2D

ถ้าต้องการให้ค่าทางซ้ายมือเป็น -10 ค่าทางขวามือเป็น 10 ค่าด้านล่างเป็น -10 และค่าด้านบนเป็น 10 ต้องสั่งคำสั่งดังนี้

gluOrtho2D(-10, 10, -10, 10)

ถ้าต้องการให้ค่าทางซ้ายมือเป็น 0 ค่าทางขวามือเป็น 100 ค่าด้านล่างเป็น 0 และค่าด้านบนเป็น 100 ต้องสั่งคำสั่งดังนี้

gluOrtho2D(0,100,0,100)

ความแตกต่างระหว่างคำสั่ง gluOrthor2D กับ glViewport เป็นดังภาพที่ 5 และในการแสดงผลต้องแปลงจากปริภูมิ 2 มิติให้เป็นวิวพอร์ตดังภาพที่ 6

ภาพที่ 5 (ซ้าย) พิกัดในปริภูมิ 2 มิติ (ขวา) พิกัดบนจอแสดงผล หรือวิวพอร์ต
ภาพที่ 6 การแปลงพิกัดจากปริภูมิ 2 มิติมาเป็นพิกัดจอแสดงผล

การแสดงผลด้วยปริภูมิ 3 มิติ

ปริภูมิ 3 มิติอ้างอิงพิกัดของจุดของรูปทรงหรือวัตถุโดยใช้ค่าตำแหน่งในแกน X ค่าตำแหน่งในแกน Y และค่าตำแหน่งในแกน Z เป็นค่าอ้างอิง ดังภาพที่ 7 ดังนั้น เมื่อนำข้อมูลในปริภูมิ 3 มิติมาแสดงผลในจอแสดงผลที่เป็น 2 มิติ จึงต้องทำการแปลงพิกัดของจุดต่างๆ และเลือกจุดที่อยู่ด้านหน้าสุด (ในโอเพนจีแอลเรียกวิธีการเลือกนี้ว่า z-test) เพื่อใช้เป็นค่าของเม็ดสีที่มองเห็น ซึ่งวิธีการแปลงเป็นดังภาพที่ 8

ภาพที่ 7 การอ้างอิงพิกัดในปริภูมิ 3 มิติของโอเพนจีแอล
ภาพที่ 8 จุดในปริภูมิ 3 มิติบนฉากรับ (จอแสดงผล) 2 มิติ
ภาพที่ 9 จุดในปริภูมิ 3 มิติบนฉากรับ (จอแสดงผล) 2 มิติที่อยู่ด้านหน้า

จากภาพที่ 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
ภาพที่ 10 รูปทรงที่เกิดจากการวาดแบบต่าง ๆ ของโอเพนจีแอล

ชุดคำสั่งสำหรับกำหนดค่าของเวอร์เท็กซ์เป็นดังนี้

โดยที่

  • x คือ ค่าพิกัดของเวอร์เท็กซ์ในแกน X
  • y คือ ค่าพิกัดของเวอร์เท็กซ์ในแกน Y
  • z คือ ค่าพิกัดของเวอร์เท็กซ์ในแกน Z

กรณีต้องการวาดสามเหลี่ยม ดังภาพที่ 11 สามารถเขียนโค้ดได้ดังนี้

ภาพที่ 11 การวาดรูปทรงสามเหลี่ยม

กรณีต้องการวาดสี่เหลี่ยม ซึ่งต้องอาศัยการวาดสามเหลี่ยมสองรูปประกบกันดังภาพที่ 4.3 สามารถเขียนโค้ดได้ดังนี้

ภาพที่ 12 การวาดรูปทรงสี่เหลี่ยมโดยการวาดจากสามเหลี่ยมสองรูป

คำสั่งที่ทำหน้าที่สั่งให้ไดรเวอร์ดำเนินการส่งข้อมูลไปยังการ์ดแสดงผลเพื่อป้องการตกค้างของคำสั่งในบัฟเฟอร์ของไดรเวอร์เป็นดังนี้

    glFlush( )

สำหรับกรณีที่ต้องการปรับให้จุดมีขนาดที่แตกต่างกัน (ปกติตั้งค่าไว้เป็น 1.0) แต่ต้องมีค่ามากกว่า 0.0 สามารถเรียกใช้คำสั่งกำหนดขนาดของจุดดังรูปแบบการใช้งานตอไปนี้

glPointSize( ขนาดของจุดที่ต้องการ )

คำสั่งสำหรับกำหนดขนาดความกว้างของเส้นมีรูปแบบการใช้งานดังนี้

glLineWidth ( ขนาดความกว้างของเส้นที่ต้องการ )

นอกจากวาดเส้นตรงเป็นเส้นต่อเนื่อง ผู้เขียนโปรแกรมสามารถวาดเส้นตรงที่มีลวดลายได้ตามความต้องการโดยเปิดใช้การทำงานของ GL_LINE_STIPPLE (และปิดเมื่อเลิกใช้) พร้อมกับกำหนดรูปแบบของลวดลายจากคำสั่งต่อไปนี้

glLineStipple( factor, pattern )

โดย

  1. pattern คือ ค่าตัวเลขขนาด 32 บิตที่ใช้แทนรูปแบบของลวดลายที่วาด ซึ่งแทนค่าบิต 1 เป็นการลงสี (on) และบิต 0 เป็นจุดว่าง (off)
  2. 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

ภาพที่ 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( แฟล็กการล้างค่าบัฟเฟอร์ )

โดย แฟล็กการล้างค่าบัฟเฟอร์เป็นชื่อบิตที่ใช้เป็นตัวแทนของประเภทบัฟเฟอร์ที่ต้องการล้างค่า ซึ่งได้แก่

  1. GL_COLOR_BUFFER_BIT สำหรับล้างค่าบัฟเฟอร์สีGL_DEPTH_BUFFER_BIT สำหรับล้างค่าบัฟเฟอร์ความลึก
  2. GL_ACCUM_BUFFER_BIT สำหรับล้างค่าบัฟเฟอร์ Accumulation
  3. 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()
ภาพที่ 14 ผลลัพธ์จาก ex02.py

ตัวอย่างโปรแกรม 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()
ภาพที่ 15 ผลลัพธ์จาก ex04.py

นอกจากนี้โอเพนจีแอลมีคำสั่งสำหรับกำหนดให้ผู้เขียนโปรแกรมเลือกให้มีการคำนวณค่าเฉพาะด้านที่ต้องการเพื่อแสดงผล เช่น เลือกให้คำนวณเฉพาะด้านหน้าของพื้นผิว ซึ่งจะทำให้ความเร็วในการทำงานสูงขึ้น แต่เมื่อใดที่ด้านหลังของพื้นผิวนั้นหันหน้าเข้าด้านที่ผู้ใช้มองจะไม่ถูกนำมาคำนวณอันส่งผลให้ไม่สามารถมองเห็นด้านหลังของพื้นผิวนั้นได้ ซึ่งรูปแบบของคำสั่งเป็นดังนี้

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()
ภาพที่ 16 ผลลัพธ์จาก ex05.py

การแสดงข้อความ

การแสดงข้อความด้วยโอเพนจีแอลนิยมกระทำกัน 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 โดยในบทความนี้ได้แนะนำพื้นฐานการเขียนโปรแกรมเปิดหน้าต่าง การวาด และการตอบสนองกับผู้ใช้ ซึ่งเป็นพื้นฐานสำหรับการศึกษาการเขียนในระดับถัดไป และสุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ

แหล่งอ้างอิง

  1. OpenGL
  2. PyOpenGL

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-06-29, 2021-06-30, 2021-10-11, 2024-04-04