Search code examples
pythonopenglpyglet

Pyglet HUD text location / scaling


Background

I have a game with a HUD I based on this example

I have a pyglet application with two gluOrtho2D views. One which is mapped 1/1 to the screen, which acts as my HUD. One which is mapped to the game world, so it scales.

Problem

When a player in the game world is rendered in a certain location, I want to display the name of this player in the HUD. But the HUD knows nothing about the world objects rendering positions, and the world knows nothing about the scaling of the screen.

Question

Is there a way to find out where something gets rendered on the screen, or is it possible to find out or simply counter-act the current scaling set in the context?

Minimal example code

Imports

from pyglet import clock, window, font
from pyglet.gl import *

Camera class that sorts out the difference between the HUD and the 2D world

class Camera(object):

    def __init__(self, win):
        self.win = win

    def project_world(self):
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluOrtho2D(-500, 1000, -500, 500)

    def project_hud(self):
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluOrtho2D(0, self.win.width, 0, self.win.height)

The HUD that contains everything that is rendered without a viewpoint.

class Hud(object):

    def __init__(self, win: window.Window):
        self.fps = clock.ClockDisplay()
        self.helv = font.load('Helvetica', 12.0)
        self.text = font.Text(self.helv, "This text does not scale", \
                              x=0, y=0, color=(1, 1, 1, 0.5))

    def draw(self):
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        self.text.draw()

The world which contains everything that is rendered from the player viewpoint

class World(object):

    def __init__(self):
        self.helv = font.load('Helvetica', 12.0)
        self.text = font.Text(self.helv, "This text should not scale", \
                              x=0, y=0, color=(1, 1, 1, 0.5))

    def draw(self):
        glClear(GL_COLOR_BUFFER_BIT)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        self.text.draw()

The application that runs the whole deal

class App(object):

    def __init__(self):
        self.win = window.Window(fullscreen=False, vsync=True, width=800, height=600)
        self.world = World()
        self.camera = Camera(self.win)
        self.hud = Hud(self.win)
        clock.set_fps_limit(60)

    def run(self):
        while not self.win.has_exit:
            self.win.dispatch_events()

            self.camera.project_world()
            self.world.draw()

            self.camera.project_hud()
            self.hud.draw()

            clock.tick()
            self.win.flip()

app = App()
app.run()

Uglyhack

For now I have solved it by ugly hacking it. Sensitive readers please do not keep on reading.

I now calculate the ratio and origo of the camera in the world and then I link the world to the HUD, so the HUD can read the attributes and calculate the position for the text itself.

This is of course Spaghetti code anti pattern which I do not want.


Solution

  • I am not a python programmer but I think i understand enough of the opengl part of the problem so I will attempt to answer. Please forgive my syntax.

    I believe you want a 2D position that represents a 3D point in your scene. You want to know the position in 2D so you can draw some text there. You have the modelview as well as the projection matrices for the 3D world. getting the 2D position is a simple matter of matrix multiplication.

    assuming my player position is p(1.0, 1.0, 1.0)

    import numpy as np
    import pyglet 
    
    #transform point to camera or eye space
    posV = np.array([1.0,1.0,1.0,1.0])
    pyglet.gl.glMatrixMode(pyglet.gl.GL_MODELVIEW)
    mvmat = (pyglet.gl.GLdouble * 16)()
    pyglet.gl.glGetDoublev(pyglet.gl.GL_MODELVIEW_MATRIX, mvmat)
    pyglet.gl.glLoadIdentity()
    npmvmat = np.array( mvmat ).reshape((4,4))
    eyeV = np.dot(npmvmat, posV)
    
    #transform point to NDC
    pyglet.gl.glMatrixMode(pyglet.gl.GL_PROJECTION)
    projmat = (pyglet.gl.GLdouble * 16)()
    pyglet.gl.glGetDoublev(pyglet.gl.GL_PROJECTION_MATRIX, projmat )
    pyglet.gl.glLoadIdentity()
    npprojmat = np.array( projmat ).reshape((4,4))
    screenV = np.dot(npprojmat, eyeV)
    
    #w-divide - convert from NDC
    screenV[0] = screenV[0] / screenV[3]
    screenV[1] = screenV[1] / screenV[3]
    
    #[-1,1] shift to [0,1]
    x = 0.5 + (0.5 * screenV[0])
    y = 0.5 + (0.5 * screenV[1])    
    
    #get a position on the viewport change to your viewport size
    width = 1920
    height = 1080
    x = x * width
    y = y * height
    
    print (x,y)
    

    finally your x and y will contain the 2D projection of the 3D point.

    Note : make sure you position glGetDoublev() calls after your matrices have been setup. store them in a variable and use it to project to 2D.

    hope this helps