1

I am new to PYopenGL, Actually, I’m also not sure if PYopenGL is the right approach for my task.

I have a 3D model in a Wavefront obj file format. I need to take a “printscreen” of the model from a given view. In other words, I would need to render the model and instead of display the model save it as an image (jpg)

My idea was to use PYopenGL for this task. However, googling I could find no suggestion or example how to do this. Therefore, I start to have doubts, if PYopenGL is the right tool for my task.

Did somebody of you already something like this or know an example that I can use to learn about?

Thanks in advance.

Michi

morgen essen
  • 21
  • 1
  • 2
  • 2
    Welcome to SO! You may want to take a look at http://stackoverflow.com/help/how-to-ask to avoid unanswered questions or down votes. Your question is very broad, and one thing we value here is that you make and show prior effort to solve your problem. As such, I suggest you add code showing what you've tried so far. If you haven't tried something yet, well we aren't here to do your job for you and you may be flagged for "too broad", but someone with just enough info may see this and guide you on your way. – gelliott181 Dec 13 '16 at 16:46

4 Answers4

9

GLUT hidden window method is much simpler, and platform independent, but it leads to window blinking.

To setup it for Django i.e. you can implement renderer as separate web server, which will blinks by window only once on start, and then returning rendered images by http response.

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

from PIL import Image
from PIL import ImageOps

import sys

width, height = 300, 300

def init():
    glClearColor(0.5, 0.5, 0.5, 1.0)
    glColor(0.0, 1.0, 0.0)
    gluOrtho2D(-1.0, 1.0, -1.0, 1.0)
    glViewport(0, 0, width, height)

def render():

    glClear(GL_COLOR_BUFFER_BIT)

    # draw xy axis with arrows
    glBegin(GL_LINES)

    # x
    glVertex2d(-1, 0)
    glVertex2d(1, 0)
    glVertex2d(1, 0)
    glVertex2d(0.95, 0.05)
    glVertex2d(1, 0)
    glVertex2d(0.95, -0.05)

    # y
    glVertex2d(0, -1)
    glVertex2d(0, 1)
    glVertex2d(0, 1)
    glVertex2d(0.05, 0.95)
    glVertex2d(0, 1)
    glVertex2d(-0.05, 0.95)

    glEnd()

    glFlush()


def draw():
    render()
    glutSwapBuffers()

def main():
    glutInit(sys.argv)

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"OpenGL Offscreen")
    glutHideWindow()

    init()
    render()

    glPixelStorei(GL_PACK_ALIGNMENT, 1)
    data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)
    image = Image.frombytes("RGBA", (width, height), data)
    image = ImageOps.flip(image) # in my case image is flipped top-bottom for some reason
    image.save('glutout.png', 'PNG')

    #glutDisplayFunc(draw)
    #glutMainLoop()

main()
Anton Ovsyannikov
  • 1,010
  • 1
  • 12
  • 30
  • Cause it still actual in 2020 i'd like to update my answer and underline, that correct way for off-screen rendering is to use (surprise!) off-screen OpenGL renderer, called OS Mesa (OS for Off-Screen). There is self-documented environment, which shows how to install it (see Dockerfile, may be little outdated, please experiment with versions) and use with PyOpenGL. https://github.com/AntonOvsyannikov/DockerGL – Anton Ovsyannikov Aug 25 '20 at 22:53
5

My answer (partially based on CodeSurgeon's answer) is for the second part of the question.

Off-screen rendering (means render something to internal buffer instead of visible window and save rendered image to file or transfer as http response to display on web page) in PyOpenGL (so as in OpenGL itself) is little tricky because everything done by GLUT so far (create window, init opengl context etc) you need to do by hands now, because you don't need the standard GLUT window to pop up or even blink.

So there are 3 methods for off-screen rendering in OpenGL:

1) Use GLUT for initialization, but hide glut window and render to it. This method is totally platform independent, but GLUT window appears for short time during initialization, so it's not so suitable for web server i.e. But you still can setup it as separate web server, which do initialization only on startup and use some interface to communicate with it.

2) Manually create everything: hidden window, OpenGL context, and Framebuffer Object to render to. This method is good, cause you control everything and no window appears, but creation of context is platform specific (below is example for Win64)

3) 3rd method is like method 2, but use default Framebuffer created by WGL, instead of creating FBO by hands. Same effects as method 2, but simpler. If you don't need FBO for some other reason, may be the preferable one.

Now I'll describe method 2, hardcore one. More samples are in my GitHub repository.

So off-screen rendering algorithm consists of following steps:

  1. Create hidden window, because you need the window, even hidden to create OpenGL context
  2. Create OpenGL context
  3. Create Framebuffer object (FBO)
  4. Create Rendering buffers (Color and Depth) and attach them to FBO (see FBO manual for details)
  5. Bind FBO to OpenGL context for rendering
  6. Render something. In this example I use only 2D primitives for simplification, but buffers are ready for 3D rendering with depth-test
  7. Setup buffers for reading, in our case there is only one FBO, so no need to choose one to read from
  8. Read rendered data from Color render buffer with glReadPixels()
  9. Do whatever you want with received data, i.e. create PIL image from it and save it to file. Also you can render with double resolution and resize PIL image for anti-aliasing effect.

So there is full example below.

Important! 3.1.1 PyOpenGL implementation have a BUG! When you just import WGL, glReadPixels() starts to crash with

ctypes.ArgumentError: argument 7: : wrong type

To avoid this go to your packages dir\OpenGL\raw\WGL_types.py , find the following lines

HANDLE = POINTER(None)  # /home/mcfletch/pylive/OpenGL-ctypes/src/wgl.h:60
# TODO: figure out how to make the handle not appear as a void_p within the code...
HANDLE.final = True

and replace it with (for x64 of course, for x86 UINT32 suppose)

HANDLE = UINT64
HANDLE.final = True

So there is example

from win32api import *
from win32con import *
from win32gui import *

from OpenGL.WGL import *
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

from PIL import Image
from PIL import ImageOps

import uuid

# =========================================
# I left here only necessary constants, it's easy to search for the rest

PFD_TYPE_RGBA =         0
PFD_MAIN_PLANE =        0
PFD_DOUBLEBUFFER =      0x00000001
PFD_DRAW_TO_WINDOW =    0x00000004
PFD_SUPPORT_OPENGL =    0x00000020

# =========================================
# OpenGL context creation helpers

def mywglCreateContext(hWnd):
    pfd = PIXELFORMATDESCRIPTOR()

    pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL
    pfd.iPixelType = PFD_TYPE_RGBA
    pfd.cColorBits = 32
    pfd.cDepthBits = 24
    pfd.iLayerType = PFD_MAIN_PLANE

    hdc = GetDC(hWnd)

    pixelformat = ChoosePixelFormat(hdc, pfd)
    SetPixelFormat(hdc, pixelformat, pfd)

    oglrc = wglCreateContext(hdc)
    wglMakeCurrent(hdc, oglrc)

    # check is context created succesfully
    # print "OpenGL version:", glGetString(GL_VERSION)


def mywglDeleteContext():
    hrc = wglGetCurrentContext()
    wglMakeCurrent(0, 0)
    if hrc: wglDeleteContext(hrc)


# =========================================
# OpenGL Framebuffer Objects helpers

def myglCreateBuffers(width, height):

    fbo = glGenFramebuffers(1)
    color_buf = glGenRenderbuffers(1)
    depth_buf = glGenRenderbuffers(1)

    # binds created FBO to context both for read and draw
    glBindFramebuffer(GL_FRAMEBUFFER, fbo)

    # bind color render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, color_buf)
    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height)
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color_buf)

    # bind depth render buffer - no need for 2D, but necessary for real 3D rendering
    glBindRenderbuffer(GL_RENDERBUFFER, depth_buf)
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height)
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth_buf)

    return fbo, color_buf, depth_buf, width, height

def myglDeleteBuffers(buffers):
    fbo, color_buf, depth_buf, width, height = buffers
    glBindFramebuffer(GL_FRAMEBUFFER, 0)
    glDeleteRenderbuffers(1, color_buf)
    glDeleteRenderbuffers(1, depth_buf)
    glDeleteFramebuffers(1, fbo)

def myglReadColorBuffer(buffers):
    fbo, color_buf, depth_buf, width, height = buffers
    glPixelStorei(GL_PACK_ALIGNMENT, 1)
    glReadBuffer(GL_COLOR_ATTACHMENT0)
    data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)
    return data, width, height

# =========================================
# Scene rendering

def renderInit(width, height):

    glClearColor(0.5, 0.5, 0.5, 1.0)
    glColor(0.0, 1.0, 0.0)
    gluOrtho2D(-1.0, 1.0, -1.0, 1.0)
    glViewport(0, 0, width, height)


def render():

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # draw xy axis with arrows
    glBegin(GL_LINES)

    # x
    glVertex2d(-1, 0)
    glVertex2d(1, 0)
    glVertex2d(1, 0)
    glVertex2d(0.95, 0.05)
    glVertex2d(1, 0)
    glVertex2d(0.95, -0.05)

    # y
    glVertex2d(0, -1)
    glVertex2d(0, 1)
    glVertex2d(0, 1)
    glVertex2d(0.05, 0.95)
    glVertex2d(0, 1)
    glVertex2d(-0.05, 0.95)

    glEnd()

    glFlush()

# =========================================
# Windows stuff and main steps

def main():

    # Create window first with Win32 API

    hInstance = GetModuleHandle(None)

    wndClass = WNDCLASS()

    wndClass.lpfnWndProc = DefWindowProc
    wndClass.hInstance = hInstance
    wndClass.hbrBackground = GetStockObject(WHITE_BRUSH)
    wndClass.hCursor = LoadCursor(0, IDC_ARROW)
    wndClass.lpszClassName = str(uuid.uuid4())
    wndClass.style = CS_OWNDC

    wndClassAtom = RegisterClass(wndClass)

    # don't care about window size, couse we will create independent buffers
    hWnd = CreateWindow(wndClassAtom, '', WS_POPUP, 0, 0, 1, 1, 0, 0, hInstance, None)

    # Ok, window created, now we can create OpenGL context

    mywglCreateContext(hWnd)

    # In OpenGL context create Framebuffer Object (FBO) and attach Color and Depth render buffers to it

    width, height = 300, 300
    buffers = myglCreateBuffers(width, height)

    # Init our renderer
    renderInit(width, height)

    # Now everything is ready for job to be done!
    # Render something and save it to file

    render()

    data, width, height = myglReadColorBuffer(buffers)
    image = Image.frombytes("RGBA", (width, height), data)
    image = ImageOps.flip(image) # in my case image is flipped top-bottom for some reason

    # it's easy to achive antialiasing effect by resizing rendered image
    # don't forget to increase initial rendered image resolution and line thikness for 2D
    #image = image.resize((width/2, height/2), Image.ANTIALIAS)

    image.save("fbo.png", "PNG")

    # Shutdown everything
    myglDeleteBuffers(buffers)
    mywglDeleteContext()

main()
Anton Ovsyannikov
  • 1,010
  • 1
  • 12
  • 30
1

In additional to glutHideWindow(), if you need do it on server, you can use virtual display. requirements.txt: pyopengl, pillow, pyvirtualdisplay. packages: freeglut3-dev, xvfb.

from pyvirtualdisplay import Display
# before glutInit create virtual display
display = Display(visible=0, size=(HEIGHT, WIDTH))
display.start()
b1oki
  • 11
  • 1
  • 3
0

PyOpenGL can be used for your purposes if all you are interested in is rendering your 3d model/scene from a specific angle to an image as long as you do not mind having an OpenGL window open and running.

There are many sources online that discuss how to write parsers for the .obj format. In addition to looking at the wikipedia article on the format here, I believe you can find an implementation of a fixed function obj loader on the pygame website. If you are making the .obj models yourself, it will be easier as the spec is pretty loose and it can be tough to write a robust parser. Alternatively, you can use libraries like Assimp to load your models and extract their data, which has the python pyAssimp bindings.

As for saving a screenshot, you need to render the 3D scene to a texture. I would strongly recommend taking a look at this answer. You would need to learn about how to use glReadPixels as well as how to use FBOs (Frame Buffer Objects) if you want to do offscreen rendering.

Community
  • 1
  • 1
CodeSurgeon
  • 2,435
  • 2
  • 15
  • 36