Reputation: 335
I'm currently programming a primary flight display using Python and OpenGL. As I have zero experience with OpenGL (and little experience with coding in general) I followed a simple tutorial giving an introduction to OpenGL for 2D applications. With this basic knowledge, I just started to make everything out of polygons (GL_QUADS) and some copied code to draw text. Which gave me this:
The visual result is satisfactory but the performance of ~450 iterations/second is questionable with one core of a Ryzen 1700x at 100% and a GTX1080 at 75% while only running this little program (which is supposed to run on a RaspberryPI 3).
So I want to try out using textures for the whole artificial horizon and inanimate objects like the scale of the speedometer, separation lines or "crosshair". I've made a .png file that would replace the artificial horizon (see bottom) but I have no idea how to render it. All the tutorials I've found are for C++ and add a lot of complexity to the overall program so I want to ask if there is an "easy" way to implement a movable texture for my application.
Here is a mockup of how I'm currently calculating the position of the horizon and how I draw it (full runnable code at the end):
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
from math import tan, radians
window_width = 800
window_height = 480
class Numbers:
mp = 0 #vertical middle point of horizon in pixels
rp = 0 #vertical right point of horizon in pixels
lp = 0 #vertical left point of horizon in pixels
def calc():
# mp- (pitch*(pixels/degree))
Numbers.mp = 240 - (Data.pitch * 8)
#offset is the vertical distance in pixels from mp
#offset =tan(roll) * pixels from horizontal middle point of screen to edge of artifical horizon
offset = tan(radians(Data.roll)) * 300
Numbers.rp = Numbers.mp - offset
Numbers.lp = Numbers.mp + offset
def horizon():
#sky
glBegin(GL_QUADS)
glColor3f(58/255, 109/255, 171/255)
glVertex2f(100, Numbers.lp)
glVertex2f(700, Numbers.rp)
glColor3f(0/255, 83/255, 165/255)
glVertex2f(700, 480)
glVertex2f(100, 480)
glEnd()
#ground
glColor3f(150/255, 70/255, 0/255)
glBegin(GL_QUADS)
glVertex2f(100, 0)
glVertex2f(700, 0)
glColor3f(140/255, 90/255, 0/255)
glVertex2f(700, Numbers.rp)
glVertex2f(100, Numbers.lp)
glEnd()
#devider line
glColor3f(255/255, 255/255, 255/255)
glBegin(GL_QUADS)
glVertex2f(100, Numbers.lp-1)
glVertex2f(700, Numbers.rp-1)
glVertex2f(700, Numbers.rp+1)
glVertex2f(100, Numbers.lp+1)
glEnd()
def iterate():
glViewport(0, 0, window_width, window_height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0.0, window_width, 0.0, window_height, 0.0, 1.0)
glMatrixMode (GL_MODELVIEW)
glLoadIdentity()
def showScreen():
#glutFullScreen()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
iterate()
calc()
horizon()
glutSwapBuffers()
if __name__ == "__main__":
glutInit()
glutInitDisplayMode(GLUT_RGBA)
glutInitWindowSize(window_width, window_height)
glutInitWindowPosition(10, 10)
wind = glutCreateWindow(b"Pi2Fly")
glutDisplayFunc(showScreen)
glutIdleFunc(showScreen)
glutMainLoop()
The real image goes from 180 to 180 to but that would be too big for SO
full code:
from OpenGL.GL import glVertex2f, glColor3f, glBegin, glEnd, GL_QUADS, GL_TRIANGLES
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
from time import sleep, perf_counter, time
from math import tan, radians
import pygame
window_width = 800
window_height = 480
pygame.font.init()
font1 = pygame.font.Font (None, 40)
font2 = pygame.font.Font (None, 20)
font3 = pygame.font.Font (None, 25)
font4 = pygame.font.Font (None, 45)
class Numbers:
mp = 0
rp = 0
lp = 0
class Data:
roll = 20
pitch = 0
airspeed = 160
altitude = 4500
altitude_last = 0
altitude_now = 0
altitude_delta = 0
altitude_delta_table = 30 * [0]
time_last = 0
time_now = 0
c1 = True
c2 = True
c3 = 0
class FPS:
c = 0
fps = 0
def fps():
FPS.c += 1
try: FPS.last_clock = FPS.this_clock
except:FPS.last_clock = perf_counter()
FPS.this_clock = perf_counter()
if round(FPS.this_clock) > round(FPS.last_clock):
FPS.fps = FPS.c
FPS.c = 0
color = (255,198,0, 255)
drawText(5, 9, str(FPS.fps), color, font1)
drawText(55, 10, "FPS", color, font2)
def get_data():
#dummy data for artifical horizon
#pitch
if Data.pitch < 20 and Data.c1: Data.pitch += 0.03
if Data.pitch >= 20 and Data.c1: Data.c1 = False
if Data.pitch < 21 and not Data.c1: Data.pitch = Data.pitch - 0.03
if Data.pitch < -20: Data.c1 = True
#roll
if Data.roll < 20 and Data.c2: Data.roll += 0.02
if Data.roll >= 20 and Data.c2: Data.c2 = False
if Data.roll < 21 and not Data.c2: Data.roll = Data.roll - 0.02
if Data.roll < -20: Data.c2 = True
#airspeed
if Data.pitch > 0: Data.airspeed = Data.airspeed - 0.004 * Data.pitch
elif Data.pitch < 0: Data.airspeed = Data.airspeed + 0.004 * abs(Data.pitch)
#altitude
if Data.pitch > 0: Data.altitude = Data.altitude + 0.01 * Data.pitch
elif Data.pitch < 0: Data.altitude = Data.altitude - 0.01 * abs(Data.pitch)
#altitude_delta
Data.time_now = perf_counter()
if Data.time_now >= Data.time_last + 0.01:
if Data.c3 == 30: Data.c3 = 0
Data.time_last = Data.time_now
Data.altitude_last = Data.altitude_now
Data.altitude_now = Data.altitude
Data.altitude_delta_table[Data.c3] = Data.altitude_now - Data.altitude_last
Data.c3 +=1
Data.altitude_delta = (sum(Data.altitude_delta_table)/len(Data.altitude_delta_table))
def calc():
Numbers.mp = 240 - (Data.pitch * 8)
offset = tan(radians(Data.roll)) * 300
Numbers.rp = Numbers.mp - offset
Numbers.lp = Numbers.mp + offset
def frame():
#frame color = grey
glColor3f(100/255, 100/255, 100/255)
#horizon right line
glBegin(GL_QUADS)
glVertex2f(99, 0)
glVertex2f(100, 0)
glVertex2f(100, window_height)
glVertex2f(99, window_height)
glEnd()
#horizon left line
glBegin(GL_QUADS)
glVertex2f(700, 0)
glVertex2f(701, 0)
glVertex2f(701, window_height)
glVertex2f(700, window_height)
glEnd()
#right upper divider
glBegin(GL_QUADS)
glVertex2f(0, 430)
glVertex2f(100, 430)
glVertex2f(100, 431)
glVertex2f(0, 431)
glEnd()
#left upper divider
glBegin(GL_QUADS)
glVertex2f(700, 430)
glVertex2f(800, 430)
glVertex2f(800, 431)
glVertex2f(700, 431)
glEnd()
#right lower divider
glBegin(GL_QUADS)
glVertex2f(0, 50)
glVertex2f(100, 50)
glVertex2f(100, 51)
glVertex2f(0, 51)
glEnd()
#left lower divider
glBegin(GL_QUADS)
glVertex2f(700, 50)
glVertex2f(800, 50)
glVertex2f(800, 51)
glVertex2f(700, 51)
glEnd()
def horizon():
#sky
glBegin(GL_QUADS)
glColor3f(58/255, 109/255, 171/255)
glVertex2f(100, Numbers.lp)
glVertex2f(700, Numbers.rp)
glColor3f(0/255, 83/255, 165/255)
glVertex2f(700, 480)
glVertex2f(100, 480)
glEnd()
#ground
glColor3f(150/255, 70/255, 0/255)
glBegin(GL_QUADS)
glVertex2f(100, 0)
glVertex2f(700, 0)
glColor3f(140/255, 90/255, 0/255)
glVertex2f(700, Numbers.rp)
glVertex2f(100, Numbers.lp)
glEnd()
#devider line
glColor3f(255/255, 255/255, 255/255)
glBegin(GL_QUADS)
glVertex2f(100, Numbers.lp-1)
glVertex2f(700, Numbers.rp-1)
glVertex2f(700, Numbers.rp+1)
glVertex2f(100, Numbers.lp+1)
glEnd()
#crosshair
#indicator triangle shadow
glColor3f(187/255, 107/255, 1/255)
glBegin(GL_QUADS)
glVertex2f(400, 230)
glVertex2f(365, 215)
glVertex2f(400, 227)
glVertex2f(435, 215)
glEnd()
# indicator triangle
glColor3f(255/255, 198/255, 0/255)
glBegin(GL_QUADS)
glVertex2f(400, 240)
glVertex2f(365, 215)
glVertex2f(400, 230)
glVertex2f(435, 215)
glEnd()
#yellow indicator line
glColor3f(255/255, 198/255, 0/255)
glBegin(GL_QUADS)
glVertex2f(320, 241)
glVertex2f(320, 239)
glVertex2f(350, 239)
glVertex2f(350, 241)
glEnd()
glBegin(GL_QUADS)
glVertex2f(450, 241)
glVertex2f(450, 239)
glVertex2f(480, 239)
glVertex2f(480, 241)
glEnd()
color = (255, 255, 255, 0)
drawText(110, 10, str("roll: "+str(round(Data.roll))), color, font3)
drawText(620, 10, str("pitch: "+str(round(Data.pitch))), color, font3)
def speedometer():
#dial
ssp = 55 #start spacer
sp = 27 #spacer
color = (255,255,255,100)
drawText(14, ssp+sp*0, "0", color, font3)
drawText(10, ssp+sp*1, "20", color, font3)
drawText(10, ssp+sp*2, "40", color, font3)
drawText(10, ssp+sp*3, "60", color, font3)
drawText(10, ssp+sp*4, "80", color, font3)
drawText(5, ssp+sp*5, "100", color, font3)
drawText(5, ssp+sp*6, "120", color, font3)
drawText(5, ssp+sp*7, "140", color, font3)
drawText(5, ssp+sp*8, "160", color, font3)
drawText(5, ssp+sp*9, "180", color, font3)
drawText(5, ssp+sp*10, "200", color, font3)
drawText(5, ssp+sp*11, "220", color, font3)
drawText(5, ssp+sp*12, "240", color, font3)
drawText(5, ssp+sp*13, "260", color, font3)
#green area
glBegin(GL_QUADS)
glColor3f(35/255, 150/255, 29/255)
glVertex2f(40, 142)
glVertex2f(99, 142)
glVertex2f(99, 287)
glVertex2f(40, 287)
glEnd()
#yellow area
glBegin(GL_QUADS)
glColor3f(150/255, 100/255, 27/255)
glVertex2f(40, 287)
glVertex2f(99, 287)
glVertex2f(99, 360)
glVertex2f(40, 360)
glEnd()
#red area
glBegin(GL_QUADS)
glColor3f(151/255, 43/255, 29/255)
glVertex2f(40, 360)
glVertex2f(99, 360)
glVertex2f(99, 374)
glVertex2f(40, 374)
glEnd()
ll=35
l=15
w=1
ssp=64
glColor3f(255/255, 255/255, 255/255)
for x in range(0, 14):
ind_lines(ll, l, w, ssp+sp*x)
#knots
color = (0, 241, 250, 255)
drawText(60, 435, "kn", color, font4)
def speedometer_indicator():
ypos = 63 + round(Data.airspeed) * 1.35
glColor3f(0/255, 241/255, 250/255)
ind_lines(40, 59, 3, ypos)
color = (0, 241, 250, 255)
drawText(5, 435, str(round(Data.airspeed)), color, font4)
def altimeter():
color = (0, 241, 250, 255)
drawText(705, 435, str(round(Data.altitude)), color, font4)
drawText(784, 435, "m", color, font3)
def variometer():
color = (0, 241, 250, 255)
drawText(710, 10, str(round((abs(Data.altitude_delta*10)), 1)), color, font4)
drawText(765, 10, "m/s", color, font3)
if Data.altitude_delta*10 > 0:
glBegin(GL_TRIANGLES)
glVertex2f(767, 30)
glVertex2f(793, 30)
glVertex2f(780, 45)
glEnd()
if Data.altitude_delta*10 < 0:
glBegin(GL_TRIANGLES)
glVertex2f(765, 45)
glVertex2f(795, 45)
glVertex2f(780, 30)
glEnd()
def ind_lines(ll, l, w, sp): #ll=left limit,l=lenght, w=width, sp=spacer
#indicator lines
glBegin(GL_QUADS)
glVertex2f(ll, sp)
glVertex2f(ll+l, sp)
glVertex2f(ll+l, sp+w)
glVertex2f(ll, sp+w)
glEnd()
def drawText(xpos, ypos, textString, color, font):
textSurface = font.render(textString, True, color, (0,0,0,1))
textData = pygame.image.tostring(textSurface, "RGBA", True)
glRasterPos2d(xpos,ypos)
glDrawPixels(textSurface.get_width(), textSurface.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, textData)
def draw_once():
speedometer()
frame()
def draw():
horizon()
speedometer_indicator()
altimeter()
variometer()
def iterate():
glViewport(0, 0, window_width, window_height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0.0, window_width, 0.0, window_height, 0.0, 1.0)
glMatrixMode (GL_MODELVIEW)
glLoadIdentity()
def showScreen():
#glutFullScreen()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
iterate()
try:get_data()
except Exception as e:print("get_data:\t", e)
try:calc()
except Exception as e:print("calc:\t", e)
try:draw_once()
except Exception as e:print("draw_once:\t", e)
try:draw()
except Exception as e:print("draw:\t", e)
try:fps()
except Exception as e:print("fps:\t", e)
glutSwapBuffers()
if __name__ == "__main__":
glutInit()
glutInitDisplayMode(GLUT_RGBA)
glutInitWindowSize(window_width, window_height)
glutInitWindowPosition(10, 10)
wind = glutCreateWindow(b"Pi2Fly")
glutDisplayFunc(showScreen)
glutIdleFunc(showScreen)
glutMainLoop()
Upvotes: 1
Views: 1008
Reputation: 210908
You can use pygame to load an texture image:
def LoadTexture(filename):
image = pygame.image.load(filename)
datas = pygame.image.tostring(image, 'RGBA')
texID = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, texID)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.get_width(), image.get_height(),
0, GL_RGBA, GL_UNSIGNED_BYTE, datas)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
glGenerateMipmap(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, 0)
return texID
Alternatively you can use Pillow and Numpy, to load a texture image, too:
from PIL import Image
import numpy
image = Image.open(filename)
imageData = numpy.array(list(image.getdata()), numpy.uint8)
texID = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, texID)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.size[0], image.size[1],
0, GL_RGBA, GL_UNSIGNED_BYTE, imageData)
# [...]
When you want to dare the texture in Legacy OpenGL, then you have to:
glTexCoord2f
, before glVertex
glBindTexture
glEnable(GL_TEXTURE_2D)
The following example, draws a quad on the entire viewport and wraps a texture on it. e.g.:
def DrawTexture(texID, x, y, w, h):
glBindTexture(GL_TEXTURE_2D, texID)
glEnable(GL_TEXTURE_2D)
glColor4f(1, 1, 1, 1)
glBegin(GL_QUADS)
glTexCoord2f(0, 1); glVertex2f(x, y)
glTexCoord2f(0, 0); glVertex2f(x, y+h)
glTexCoord2f(1, 0); glVertex2f(x+w, y+h)
glTexCoord2f(1, 1); glVertex2f(x+w, y)
glEnd()
glDisable(GL_TEXTURE_2D)
Note, if texturing is enabled, then by default the color of the texel is multiplied by the current color, because by default the texture environment mode (GL_TEXTURE_ENV_MODE
) is GL_MODULATE
. See glTexEnv
.
This causes that the color of the texels of the texture is "mixed" by the last color which you have set by glColor4f
.
Set a "white" color before you render the texture, to solve your issue:
glColor4f(1, 1, 1, 1)
Likewise you can change the environment mode to GL_REPLACE
, instead:
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
Furthermore, if the texture is partially transparent, you have to enable Blending before drawing the texture:
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
DrawTexture(....)
glDisable(GL_BLEND)
Upvotes: 1