Houdini 20.0 Examples Python panel examples

Custom Graphics

Custom OpenGL drawing in a Python Panel.

Example
<?xml version="1.0" encoding="UTF-8"?>
<pythonPanelDocument>
  <!-- This file contains definitions of Python interfaces and the
 interfaces menu.  It should not be hand-edited when it is being
 used by the application.  Note, that two definitions of the
 same interface or of the interfaces menu are not allowed
 in a single file. -->
  <interface name="CustomGraphicSceneExample" label="Graphics Scene Example" icon="SOP_torus">
    <script><![CDATA[import math
from hutil.Qt import QtCore, QtWidgets

py_gl_found = True

try:
    from OpenGL.GL import *
    from OpenGL.GL import shaders
    from OpenGL.arrays import *
except ImportError:
    py_gl_found = False
    QtWidgets.QMessageBox.critical(None, "Custom Graphics Scene Example",
                            "PyOpenGL must be installed to run this example.",
                            QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Default,
                            QtWidgets.QMessageBox.NoButton)
def float_array(py_array):
    floats = GLfloatArray.zeros(len(py_array))
    for x in range(0, len(py_array)):
        floats[x] = py_array[x]

    return floats

label_style = "background: rgba(0,0,0,0); color: #FFFFFF; font-size: 14px"
light_label_style = "background: rgba(0,0,0,0); color: #CCCCCC; font-size: 12px"

desc_text = """This example uses a custom graphics scene to combine QWidgets with \
OpenGL rendering.
Use the left mouse button to pan and the mouse wheel to zoom."""

box_verts = [-1, 1, -1, 1, 1, 1, 1, 1, -1, -1,  1, -1, -1, 1, 1, 1, 1, 1,
             -1, 1, -1, 1, 1, -1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1,
             -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1,-1, -1, 1,
             -1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1,
              1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1,
             -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, -1, -1,
            ]

class CustomScene(QtWidgets.QGraphicsScene):
    def __init__(self):
        """Initializes the custom graphics scene. Sets up labels and default
           values for rotation and zoom factors."""

        QtWidgets.QGraphicsScene.__init__(self)
        self.xRot = 35.0
        self.yRot = 25.0
        self.lastPos = None
        self.zoom = 1.0

        self.rotationLabel = QtWidgets.QLabel("Rotation:  [35.0, 25.0]")
        self.rotationLabel.move(10, 10)
        self.rotationLabel.setFixedWidth(200)
        self.rotationLabel.setStyleSheet(label_style)

        self.zoomLabel = QtWidgets.QLabel("Zoom factor:  [1.0]")
        self.zoomLabel.move(10, 30)
        self.zoomLabel.setFixedWidth(200)
        self.zoomLabel.setStyleSheet(label_style)

        self.descLabel = QtWidgets.QLabel(desc_text) 
        self.descLabel.move(10, 200)
        self.descLabel.setStyleSheet(light_label_style)
        self.descLabel.setFixedWidth(self.width())
        self.descLabel.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom)

        self.resetButton = QtWidgets.QPushButton("Reset Orientation")
        self.resetButton.move(10, 70)
        self.resetButton.clicked.connect(self.reset)

        self.addWidget(self.rotationLabel)
        self.addWidget(self.zoomLabel)
        self.addWidget(self.descLabel)
        self.addWidget(self.resetButton)

        self.glInit = False
        self.shader = None

    def reset(self):
        self.xRot = 35.0
        self.yRot = 25.0
        self.zoom = 1.0
        self.rotationLabel.setText("Rotation:  [35.0, 25.0]")        
        self.zoomLabel.setText("Zoom factor:  [1.0]")
        self.update()

    def wheelEvent(self, event):
        """Called when the mouse wheel is rotated. Increases or decreases
           the zoom factor based on the mouse wheel."""

        QtWidgets.QGraphicsScene.wheelEvent(self, event)

        if event.isAccepted():
            return

        self.zoom -= event.delta() / 2000.0
        if self.zoom < 0.1:
            self.zoom = 0.1

        self.zoomLabel.setText("Zoom factor:  [" + str(self.zoom) + "]")
        self.update()

    def mousePressEvent(self, event):
        """ Called when a mouse button is pressed. Stores the mouse pos."""

        QtWidgets.QGraphicsScene.mousePressEvent(self, event)

        if event.buttons() == QtCore.Qt.LeftButton:
            self.lastPos = event.scenePos()

    def mouseMoveEvent(self, event):
        """Called when the mouse moves. Rotates the viewport"""

        QtWidgets.QGraphicsScene.mouseMoveEvent(self, event)

        if event.isAccepted():
            return

        if event.buttons() == QtCore.Qt.LeftButton:
            pos = event.scenePos()
            if self.lastPos is None:
                self.lastPos = pos
            else:
                deltaX = pos.x() - self.lastPos.x()
                deltaY = pos.y() - self.lastPos.y()
                self.lastPos = pos

                self.yRot += deltaX
                self.xRot += deltaY

                if self.xRot > 90:
                    self.xRot = 90
                elif self.xRot < -90:
                    self.xRot = -90

                if self.yRot > 180:
                    self.yRot -= 360
                elif self.yRot < -180:
                    self.yRot += 360

                self.rotationLabel.setText("Rotation:  [" + str(self.xRot) +
                                           ", " + str(self.yRot) + "]")
                self.update()

    def initGL(self):
        """ Initializes OpenGL resources if they have not already been created"""
        if self.glInit:
            return

        self.glInit = True

        verts = float_array(box_verts)

        self.vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
        glBufferData(GL_ARRAY_BUFFER, 432, verts, GL_STATIC_DRAW)
        glBindBuffer(GL_ARRAY_BUFFER, 0)

        vertex_shader = shaders.compileShader("""
            #version 150
            in vec3 v_position;

            uniform mat4 m_scale;
            uniform mat4 m_rot;
            uniform mat4 m_projection;

            void main()
            {
                mat4 mvp = m_projection * m_rot * m_scale;
                gl_Position = mvp * vec4(v_position, 1.0);
            }
            """, GL_VERTEX_SHADER)

        fragment_shader = shaders.compileShader("""
            #version 150

            uniform float f_opacity;
            void main()
            {
                gl_FragColor = vec4(0.9, 0.55, 0.3, f_opacity);
            }
            """, GL_FRAGMENT_SHADER)

        self.shader = shaders.compileProgram(vertex_shader, fragment_shader)

        self.uniforms = {
            'm_scale': glGetUniformLocation(self.shader, 'm_scale'),
            'm_rot': glGetUniformLocation(self.shader, 'm_rot'),
            'm_projection': glGetUniformLocation( self.shader, 'm_projection'),
            'f_opacity': glGetUniformLocation( self.shader, 'f_opacity'),
        }

    def setProjection(self):
        """ Computes and sets the projection matrix based on view size/zoom"""
        aspect = float(self.width())/float(self.height())
        factor = 5 * self.zoom

        lr = 1.0 / (factor * aspect * 2.0);
        bt = 1.0 / (factor * 2.0);
        nf = 1.0 / (-1.0 - 100.0);

        ortho = [lr, 0,  0,         0,
                 0,  bt, 0,         0,
                 0,  0,  2.0 * nf,  0, 
                 0,  0,  99.0 * nf, 1]
        p = float_array(ortho)

        glUniformMatrix4fv(self.uniforms["m_projection"], 1, GL_FALSE, p)

    def drawBox(self, x_scale, y_scale, z_scale, opacity):
        # Transform the box based on the view settings and scale        
        ca = math.cos(0.0)
        cb = math.cos(self.xRot/ 180.0 * 3.14159)
        ch = math.cos(self.yRot/ 180.0 * 3.14159)

        sa = math.sin(0.0)
        sb = math.sin(self.xRot/ 180.0 * 3.14159)
        sh = math.sin(self.yRot/ 180.0 * 3.14159)

        r = [ch*ca,  -ch*sa*cb + sh*sb,  ch*sa*sb + sh*cb,    0.0,
             sa,     ca*cb,              -ca*sb,              0.0,
             -sh*ca, sh*sa*cb + ch*sb,   (-sh*sa*sb + ch*cb), 0.0, 
             0.0,    0.0,                -3.0,                1.0]
        rot = float_array(r)

        s = [x_scale, 0.0,     0.0,     0.0,
             0.0,     y_scale, 0.0,     0.0,
             0.0,     0.0,     z_scale, 0.0,
             0.0,     0.0,     0.0,     1.0]
        scale = float_array(s)

        glUniformMatrix4fv(self.uniforms["m_scale"], 1, GL_FALSE, scale)
        glUniformMatrix4fv(self.uniforms["m_rot"], 1, GL_FALSE, rot)

        glUniform1fv(self.uniforms["f_opacity"], 1, opacity)

        glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
        glEnableClientState(GL_VERTEX_ARRAY)
        glVertexPointer(3, GL_FLOAT, 0, None)

        glDrawArrays(GL_TRIANGLES, 0, 36)

        glDisableClientState(GL_VERTEX_ARRAY)
        glBindBuffer (GL_ARRAY_BUFFER, 0)

    def drawBackground(self, painter, rect):
        """ Called when the scene should render it's background. Can use GL."""

        QtWidgets.QGraphicsScene.drawBackground(self, painter, rect)

        # PyOpenGL is needed for this example
        if not py_gl_found:
            return

        # Init GL
        self.initGL()

        # Update the label to match the size of the graphics scene
        self.descLabel.setFixedWidth(self.width())
        self.descLabel.setFixedHeight(self.height() - 230)

        # Clear the viewport
        glClearColor(0.2, 0.28, 0.32, 1.0)
        glClearDepth(1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        # Enable blending and culling
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)
        glEnable(GL_CULL_FACE)

        # Activate the shader for the box
        shaders.glUseProgram(self.shader)

        # Set the projection matrix
        self.setProjection()

        # Draw the boxes
        self.drawBox(0.50, 0.10, 0.25, 0.75)
        self.drawBox(0.75, 0.25, 0.75, 0.45)
        self.drawBox(1.75, 0.50, 1.75, 0.25)
        self.drawBox(2.50, 1.50, 2.50, 0.10)

        # Unbind the shader
        shaders.glUseProgram(0)

        # Restore the GL state
        glDisable(GL_BLEND)
        glDisable(GL_CULL_FACE)

def onCreateInterface():
    widget = CustomScene()
    return widget
]]></script>
  </interface>
</pythonPanelDocument>

Python panel examples

  • Custom Graphics

    Custom OpenGL drawing in a Python Panel.

  • Drag and Drop

    How to implement drag and drop functionality in a Python Panel.

  • Linked parameters

    How to link PySide parameter widgets (i.e. text fields and sliders) to Houdini node parameters and vice versa.

  • Node path

    How to listen for changes to the current node path.

  • Qt designer

    How to load user interface layout from a Qt Designer file.

  • Qt events

    How Python Panels can listen to Qt events.

  • Viewport color editor

    A PySide interface for editing viewport colors.