Source code for opengl.renderer

#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
"""opengl/renderer.py

Python OpenGL framework for pyFormex

This OpenGL framework is intended to replace (in due time)
the current OpenGL framework in pyFormex.

(C) 2013 Benedict Verhegghe and the pyFormex project.

"""
import numpy as np

import pyformex as pf
from pyformex.attributes import Attributes
from .matrix import Matrix4
from .camera import orthogonal_matrix
from .gl import GL

if not pf.sphinx and pf.options.gl3:
    from opengl3.shader import Shader
else:
    from .shader import Shader


class Renderer():

    def __init__(self, canvas, shader=None):
        self.canvas = canvas
        if shader is None:
            shader = Shader(self.canvas)
        self.shader = shader
        self.camera = self.canvas.camera


    def loadLightProfile(self):
        lightprof = self.canvas.lightprof
        mat = self.canvas.material
        lights = [light for light in lightprof.lights if light.enabled]
        nlights = len(lights)
        ambient = lightprof.ambient * np.ones(3)  # global ambient
        for light in lights:
            ambient += light.ambient
        ambient = np.clip(ambient, 0., 1.)  # clip, OpenGL does anyways
        diffuse = np.array([light.diffuse for light in lights]).ravel()
        specular = np.array([light.specular for light in lights]).ravel()
        position = np.array([light.position[:3] for light in lights]).ravel()
        settings = Attributes({
            'nlights': nlights,
            'ambicolor': ambient,
            'diffcolor': diffuse,
            'speccolor': specular,
            'lightdir': position,
            'ambient': mat.ambient,
            'diffuse': mat.diffuse,
            'specular': mat.specular,
            'shininess': mat.shininess,
            'alphablend': self.canvas.settings.alphablend,
            'alpha': self.canvas.settings.transparency,
            'bkalpha': self.canvas.settings.transparency,
            })
        #if pf.options.gl3:
            #print("LIGHT PROFILE to shader: %s" % settings)
        self.shader.loadUniforms(settings)


    def setDefaults(self):
        """Set the GL context and the shader uniforms to default values."""
        GL.glLineWidth(self.canvas.settings.linewidth)
        # Enable setting pointsize in the shader
        # Maybe we should do pointsize with gl context?
        GL.glEnable(GL.GL_VERTEX_PROGRAM_POINT_SIZE)
        # NEXT, the shader uniforms
        # When all attribute names are correct, this could be done with a
        # single statement like
        #   self.shader.loadUniforms(self.canvas.settings)
        #
        self.shader.uniformInt('highlight', False)
        self.shader.uniformFloat('lighting', self.canvas.settings.lighting)
        self.shader.uniformInt('useObjectColor', 1)
        self.shader.uniformInt('rgbamode', False)
        self.shader.uniformVec3('objectColor', self.canvas.settings.fgcolor)
        self.shader.uniformVec3('objectBkColor', self.canvas.settings.fgcolor)
        self.shader.uniformVec3('highlightColor', self.canvas.settings.slcolor)
        self.shader.uniformFloat('pointsize', self.canvas.settings.pointsize)
        self.loadLightProfile()


    def renderObjects(self, objects):
        """Render a list of objects"""
        for obj in objects:
            GL.glDepthFunc(GL.GL_LESS)
            self.setDefaults()
            if obj.trl or obj.rot or obj.trl0:
                self.loadMatrices(rot=self.rot, trl=self.trl, trl0=self.trl0)
            obj.render(self)
            if obj.trl or obj.rot or obj.trl0:
                self.loadMatrices()


    def pickObjects(self, objects):
        """Render a list of objects for picking"""
        for obj in objects:
            GL.glDepthFunc(GL.GL_LESS)
            #self.setDefaults()
            if obj.trl or obj.rot or obj.trl0:
                self.loadMatrices(rot=self.rot, trl=self.trl, trl0=self.trl0)
            obj.renderpick(self)
            if obj.trl or obj.rot or obj.trl0:
                self.loadMatrices()

    def loadMatrices(self, rot=None, trl=None, trl0=None):
        """Load the rendering matrices.

        rot, trl, trl0 are additional rotation, translation and
        pre-translation for the object.
        """
        # Get the current modelview*projection matrix
        modelview = self.camera.modelview
        if trl0:
            modelview.translate(trl0)
        if rot:
            modelview.rotate(rot)
        if trl:
            modelview.translate(trl)
        projection = self.camera.projection
        transinv = self.camera.modelview.transinv().rot

        # Propagate the matrices to the uniforms of the shader
        self.shader.uniformMat4('modelview', modelview.gl())
        self.shader.uniformMat4('projection', projection.gl())
        self.shader.uniformMat3('normalstransform', transinv.flatten().astype(np.float32))


    def render3D(self, actors, picking=False):
        """Render the 3D actors in the scene.

        """
        if not actors:
            return

        # Get the current modelview*projection matrix
        modelview = self.camera.modelview
        projection = self.camera.projection
        transinv = self.camera.modelview.transinv().rot

        # Propagate the matrices to the uniforms of the shader
        self.shader.uniformMat4('modelview', modelview.gl())
        self.shader.uniformMat4('projection', projection.gl())
        self.shader.uniformMat3('normalstransform', transinv.flatten().astype(np.float32))

        # sort actors in back and front, and select visible
        actors = back(actors) + front(actors)
        actors =  [o for o in actors if o.visible is not False]

        GL.glEnable(GL.GL_DEPTH_TEST)
        GL.glDepthMask(GL.GL_TRUE)

        if picking:
            # render with pickColor
            actors = [a for a in actors if a.pickable]
            self.pickObjects(actors)

        elif not self.canvas.settings.alphablend:
            # opaque: all actors in order of creation
            self.renderObjects(actors)

        else:
            # alphablend: optimize drawing order
            opaque = [a for a in actors if a.opak]
            transp = [a for a in actors if not a.opak]

            # First render the opaque objects
            self.renderObjects(opaque)

            # Then the transparent ones
            # Disable writing to the depth buffer
            # as everything behind the transparent object
            # also needs to be drawn
            GL.glDepthMask(GL.GL_FALSE)

            if pf.cfg['render/transp_nocull']:
                GL.glDisable(GL.GL_CULL_FACE)

            GL.glEnable(GL.GL_BLEND)
            GL.glBlendEquation(GL.GL_FUNC_ADD)
            if pf.cfg['render/alphablend'] == 'mult':
                GL.glBlendFunc(GL.GL_ZERO, GL.GL_SRC_COLOR)
            elif pf.cfg['render/alphablend'] == 'add':
                GL.glBlendFunc(GL.GL_ONE, GL.GL_ONE)
            elif pf.cfg['render/alphablend'] == 'trad1':
                GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE)
            else:
                GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
            self.renderObjects(transp)
            GL.glDisable(GL.GL_BLEND)

        GL.glDepthMask(GL.GL_TRUE)


    def render2D(self, actors):
        """Render 2D decorations.

        """
        if not actors:
            return
        # Set modelview/projection
        modelview = Matrix4()
        self.shader.uniformMat4('modelview', modelview.gl())

        left = 0.  # -0.5
        right = float(self.canvas.width())  # -0.5
        bottom = 0.  # -0.5
        top = float(self.canvas.height())  # -0.5
        near = -1.
        far = 1.
        projection = orthogonal_matrix(left, right, bottom, top, near, far)
        self.shader.uniformMat4('projection', projection.gl())

        GL.glDisable(GL.GL_DEPTH_TEST)
        GL.glDisable(GL.GL_CULL_FACE)
        GL.glDepthMask(GL.GL_FALSE)

        # ALPHABLEND
        #
        # We need blending for text rendering!
        #
        # 2D actors are by default opak!
        opaque = [a for a in actors if a.opak is None or a.opak]
        transp = [a for a in actors if a.opak is False]

        # First render the opaque objects
        self.renderObjects(opaque)

        # Then the transparent ones
        # Disable writing to the depth buffer
        # as everything behind the transparent object
        # also needs to be drawn
        # TODO: put this in a function enableDepth(depthmode)
        #   as it is also used in render3D
        GL.glEnable(GL.GL_BLEND)
        GL.glBlendEquation(GL.GL_FUNC_ADD)
        if pf.cfg['render/textblend'] == 'mult':
            GL.glBlendFunc(GL.GL_ZERO, GL.GL_SRC_COLOR)
        elif pf.cfg['render/textblend'] == 'add':
            GL.glBlendFunc(GL.GL_ONE, GL.GL_ONE)
        elif pf.cfg['render/textblend'] == 'trad1':
            GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE)
        elif pf.cfg['render/textblend'] == 'zero':
            GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ZERO)
        else:
             GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
        self.renderObjects(transp)
        GL.glDisable(GL.GL_BLEND)
        GL.glDepthMask(GL.GL_TRUE)


    def renderBG(self, actors):
        """Render 2D background actors.

        """
        if not actors:
            return

        #print("Render %s backgrounds" % len(actors))

        # Set modelview/projection
        modelview = projection = Matrix4()

        self.shader.uniformMat4('modelview', modelview.gl())
        self.shader.uniformMat4('projection', projection.gl())

        self.canvas.zoom_2D()  # should be combined with above

        # in clip space
        GL.glDisable(GL.GL_DEPTH_TEST)
        self.renderObjects(actors)


    def render(self, scene, picking=False):
        """Render the geometry for the scene."""
        self.shader.bind(picking=picking)

        try:
            if picking:
                # can only pick from actors
                self.render3D(scene.actors, picking)
            else:
                # The back backgrounds
                self.renderBG(back(scene.backgrounds))
                # The 2D back decorations
                self.render2D(back(scene.decorations + scene.annot2d))
                # The 3D stuff
                self.render3D(scene.actors + scene.annot3d, picking)
                # The 2D front decorations
                self.render2D(front(scene.annot2d + scene.decorations))
                # The front backgrounds
                self.renderBG(front(scene.backgrounds))

        finally:
            self.shader.unbind()


[docs]def front(actorlist): """Return the actors from the list that have ontop=True""" return [a for a in actorlist if a.ontop]
[docs]def back(actorlist): """Return the actors from the list that have ontop=False""" return [a for a in actorlist if not a.ontop]
# End