view pytouhou/ui/opengl/gamerenderer.pyx @ 612:73f134f84c7f

Request a RGB888 context, since SDL2’s default of RGB332 sucks. On X11/GLX, it will select the first config available, that is the best one, while on EGL it will iterate over them to select the one closest to what the application requested. Of course, anything lower than RGB888 looks bad and we really don’t want that.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Thu, 26 Mar 2015 20:20:37 +0100
parents 1b31169dc344
children a6af3ff86612
line wrap: on
line source

# -*- encoding: utf-8 -*-
##
## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
##
## 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; version 3 only.
##
## 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.
##

from libc.stdlib cimport malloc, free
from itertools import chain

from pytouhou.lib.opengl cimport \
         (glClear, glMatrixMode, glLoadIdentity, glLoadMatrixf, glDisable,
          glEnable, glFogi, glFogf, glFogfv, GL_PROJECTION, GL_MODELVIEW,
          GL_FOG, GL_FOG_MODE, GL_LINEAR, GL_FOG_START, GL_FOG_END,
          GL_FOG_COLOR, GL_COLOR_BUFFER_BIT, GLfloat, glViewport, glScissor,
          GL_SCISSOR_TEST, GL_DEPTH_BUFFER_BIT, glPushDebugGroup,
          GL_DEBUG_SOURCE_APPLICATION, glPopDebugGroup, glBindTexture,
          glGetTexImage, GL_TEXTURE_2D, GL_RGB, GL_UNSIGNED_BYTE)

from pytouhou.utils.matrix cimport mul, new_identity
from pytouhou.utils.maths cimport perspective, setup_camera, ortho_2d
from pytouhou.game.text cimport NativeText, GlyphCollection
from pytouhou.ui.window cimport Window
from .shaders.eosd import GameShader, BackgroundShader
from .renderer cimport Texture
from .backend cimport is_legacy, use_debug_group, use_pack_invert, use_scaled_rendering

from collections import namedtuple
Rect = namedtuple('Rect', 'x y w h')
Color = namedtuple('Color', 'r g b a')


cdef class GameRenderer(Renderer):
    def __init__(self, resource_loader, Window window):
        Renderer.__init__(self, resource_loader)

        if not is_legacy:
            self.game_shader = GameShader()
            self.background_shader = BackgroundShader()
            self.interface_shader = self.game_shader

        if use_scaled_rendering:
            self.framebuffer = Framebuffer(0, 0, window.width, window.height)


    def __dealloc__(self):
        if self.game_mvp != NULL:
            free(self.game_mvp)
        if self.interface_mvp != NULL:
            free(self.interface_mvp)
        if self.proj != NULL:
            free(self.proj)


    property size:
        # We never need to get back the computed size, so size is write-only.
        def __set__(self, tuple size):
            self.x, self.y, self.width, self.height = size


    def load_textures(self, dict anms):
        self.texture_manager.load(anms)


    def load_background(self, background):
        self.background = background
        if background is not None:
            self.background_renderer = BackgroundRenderer()
            self.background_renderer.load(background, self.textures)
        else:
            self.background_renderer = None


    def start(self, common):
        self.proj = perspective(30, float(common.width) / float(common.height),
                                101010101./2010101., 101010101./10101.)
        self.game_mvp = setup_camera(0, 0, 1)
        mul(self.game_mvp, self.proj)
        self.interface_mvp = ortho_2d(0., float(common.interface.width),
                                      float(common.interface.height), 0.)


    def render(self, Game game):
        if use_scaled_rendering:
            self.framebuffer.bind()

        self.render_game(game)
        self.render_text(game.texts)
        self.render_interface(game.interface, game.boss)

        if use_scaled_rendering:
            self.framebuffer.render(self.x, self.y, self.width, self.height)


    def capture(self, filename, int width, int height):
        capture_memory = <char*>malloc(width * height * 3)

        glBindTexture(GL_TEXTURE_2D, self.framebuffer.texture)
        glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, capture_memory)
        glBindTexture(GL_TEXTURE_2D, 0)

        # TODO: output to PNG instead.

        # PPM output, bottom to top.
        with open(filename, 'wb') as ppm:
            ppm.write(('P6\n%d %d\n 255\n' % (width, height)).encode())
            if use_pack_invert:
                ppm.write(capture_memory[:width * height * 3])
            else:
                for i in range(width * (height - 1), -1, -width):
                    ppm.write(capture_memory[i * 3:(i + width) * 3])

        # Cleanup.
        free(capture_memory)


    cdef void render_game(self, Game game) except *:
        cdef long game_x, game_y
        cdef float x, y, z, dx, dy, dz
        cdef float fog_data[4]
        cdef float fog_start, fog_end
        cdef unsigned char fog_r, fog_g, fog_b
        cdef Matrix *mvp

        if use_debug_group:
            glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0, -1, "Game rendering")

        game_x, game_y = game.interface.game_pos
        glViewport(game_x, game_y, game.width, game.height)
        glClear(GL_DEPTH_BUFFER_BIT)
        glScissor(game_x, game_y, game.width, game.height)
        glEnable(GL_SCISSOR_TEST)

        if is_legacy:
            glMatrixMode(GL_PROJECTION)
            glLoadIdentity()

        if self.background_renderer is None:
            glClear(GL_COLOR_BUFFER_BIT)
        elif game is not None and game.spellcard_effect is not None:
            if is_legacy:
                glMatrixMode(GL_MODELVIEW)
                glLoadMatrixf(<GLfloat*>self.game_mvp)
                glDisable(GL_FOG)
            else:
                self.game_shader.bind()
                self.game_shader.uniform_matrix('mvp', self.game_mvp)

            self.render_elements([game.spellcard_effect])
        else:
            back = self.background
            x, y, z = back.position_interpolator.values
            dx, dy, dz = back.position2_interpolator.values
            fog_b, fog_g, fog_r, fog_start, fog_end = back.fog_interpolator.values

            # Those two lines may come from the difference between Direct3D and
            # OpenGL’s distance handling.  The first one seem to calculate fog
            # from the eye, while the second does that starting from the near
            # plane.
            #TODO: investigate, and use a variable to keep the near plane
            # distance at a single place.
            fog_start -= 101010101./2010101.
            fog_end -= 101010101./2010101.

            mvp = new_identity()
            mvp_data = <GLfloat*>mvp
            mvp_data[12] = -x
            mvp_data[13] = -y
            mvp_data[14] = -z
            view = setup_camera(dx, dy, dz)
            mul(mvp, view)
            free(view)
            mul(mvp, self.proj)

            if is_legacy:
                glMatrixMode(GL_MODELVIEW)
                glLoadMatrixf(mvp_data)

                glEnable(GL_FOG)
                glFogi(GL_FOG_MODE, GL_LINEAR)
                glFogf(GL_FOG_START, fog_start)
                glFogf(GL_FOG_END,  fog_end)

                fog_data[0] = fog_r / 255.
                fog_data[1] = fog_g / 255.
                fog_data[2] = fog_b / 255.
                fog_data[3] = 1.
                glFogfv(GL_FOG_COLOR, fog_data)
            else:
                self.background_shader.bind()
                self.background_shader.uniform_matrix('mvp', mvp)

                self.background_shader.uniform_1('fog_scale', 1. / (fog_end - fog_start))
                self.background_shader.uniform_1('fog_end', fog_end)
                self.background_shader.uniform_4('fog_color', fog_r / 255., fog_g / 255., fog_b / 255., 1.)

            free(mvp)
            self.background_renderer.render_background()

        if game is not None:
            if is_legacy:
                glMatrixMode(GL_MODELVIEW)
                glLoadMatrixf(<GLfloat*>self.game_mvp)
                glDisable(GL_FOG)
            else:
                self.game_shader.bind()
                self.game_shader.uniform_matrix('mvp', self.game_mvp)

            self.render_elements([enemy for enemy in game.enemies if enemy.visible])
            self.render_elements(game.effects)
            self.render_elements(chain(game.players_bullets,
                                       game.lasers_sprites(),
                                       game.players,
                                       game.msg_sprites()))
            self.render_elements(chain(game.bullets, game.lasers,
                                       game.cancelled_bullets, game.items,
                                       game.labels))

        if game.msg_runner is not None:
            rect = Rect(48, 368, 288, 48)
            color1 = Color(0, 0, 0, 192)
            color2 = Color(0, 0, 0, 128)
            self.render_quads([rect], [(color1, color1, color2, color2)], 0)

        glDisable(GL_SCISSOR_TEST)

        if use_debug_group:
            glPopDebugGroup()


    cdef void render_text(self, dict texts) except *:
        cdef NativeText label

        if self.font_manager is None:
            return

        self.font_manager.load(texts)

        black = Color(0, 0, 0, 255)

        for label in texts.values():
            texture = (<Texture>label.texture).texture
            rect = Rect(label.x, label.y, label.width, label.height)
            gradient = [Color(*color, a=label.alpha) for color in label.gradient]

            if label.shadow:
                shadow_rect = Rect(label.x + 1, label.y + 1, label.width, label.height)
                shadow = [black._replace(a=label.alpha)] * 4
                self.render_quads([shadow_rect, rect], [shadow, gradient], texture)
            else:
                self.render_quads([rect], [gradient], texture)


    cdef void render_interface(self, interface, game_boss) except *:
        cdef GlyphCollection label

        elements = []

        if use_debug_group:
            glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0, -1, "Interface rendering")

        if is_legacy:
            glMatrixMode(GL_MODELVIEW)
            glLoadMatrixf(<GLfloat*>self.interface_mvp)
            glDisable(GL_FOG)
        else:
            self.interface_shader.bind()
            self.interface_shader.uniform_matrix('mvp', self.interface_mvp)
        glViewport(0, 0, interface.width, interface.height)

        items = [item for item in interface.items if item.anmrunner and item.anmrunner.running]
        labels = interface.labels.values()

        if items:
            # Redraw all the interface
            elements.extend(items)
        else:
            # Redraw only changed labels
            labels = [label for label in labels if label.changed]

        elements.extend(interface.level_start)

        if game_boss is not None:
            elements.extend(interface.boss_items)

        elements.extend(labels)
        self.render_elements(elements)
        for label in labels:
            label.changed = False

        if use_debug_group:
            glPopDebugGroup()