Кроссплатформенные OpenGL + Python при помощи Kivy

    Предыстория: будучи наивным чукотским программистом, я думал: "питон такой кроссплатформенный, напишу игрушку для сына, запущу на планшетике, пусть играется". В результате две недели ушло на попытки натыкать решение по переезду с PyOpenGL+pygame на kivy, так как внятного примера использования OpenGL с kivy не нашел. Возможно, кому-то мой опыт поможет сэкономить время.

    Примерная схема процесса
    Примерная схема процесса

    Дисклеймер:

    1. Никого не призываю разрабатывать на python приложения с 3д-графикой под андроид. Это не самый шустрый вариант. Но если очень хочется, то можно читать дальше.

    2. В kivy есть встроенное-своё-родное решение Mesh, которое умеет в 3д-графику. С примерами тоже не всё так просто. Я предпочел голый OpenGL.

    3. Возможно, я открыл Америку в попытках найти Индию. Надеюсь, более опытные товарищи поправят и дополнят.

    Перед тем, как начать работать с 3D графикой в kivy, стоит почитать про библиотеку вообще и про её установку (например, мануалы рекомендуют использовать виртуальную среду).

    В чем проблема? Описание функций OpenGL разработчики kivy коварно прячут на своём официальном сайте. Вопрос только в том, как заставить это работать.

    За исключением нескольких нюансов, переезд с PyOpenGL+pygame на kivy касательно использования функций собственно OpenGL заключается в замене строчек:

    from OpenGL.GL import *
    from OpenGL.GL.shaders import *

    на:

    from kivy.graphics.opengl import *

    Нюансы: PyOpenGL работает с массивами numpy напрямую, в kivy приходится преобразовывать их функцией tobytes(). Для загрузки текстур в kivy используется glPixelStorei и glTexImage2D вместо glTexStorage2D и glTexSubImage2D. Шейдеры под андроид должны быть под версию 2 (без in, out и прочих излишеств) и иметь в начале код:

    #ifdef GL_ES
        precision highp float;
    #endif

    Далее самое интересное - это всё надо как-то подключить к выводу на экран. В связке PyOpenGL+pygame код имеет такую структуру:

    #Было:
    import pygame
    from pygame.locals import *
    
    def init():
        pygame.init()
        pygame.display.set_mode((Width, Height), HWSURFACE | OPENGL | DOUBLEBUF)
        ''' дальше идут функции, инициализирующие работу с PyOpenGL '''
        
    def main():
        init()
        while True:
            ''' меняем вершины и запускаем отрисовку '''
            pygame.display.flip()

    В kivy надо создать класс приложения на основе класса App, в котором необходимо сослаться на виджет, унаследованный от класса Widget. Кроме того, нам понадобится объект Window, который автоматически создается при работе с kivy с учетом используемой операционной системы. Базовый класс у Window - WindowBase.

    Таким образом структура приложения будет примерно такой:

    #Стало:
    from kivy.app import App
    from kivy.uix.widget import Widget
    from kivy.core.window import Window
    from kivy.base import EventLoop
    from kivy.clock import Clock
    
    def init():
        ''' почти те же функции, инициализирующие работу с OpenGL '''
    init()
    
    class CustomWidget(Widget):
        def __init__(self, **kwargs):
            super(CustomWidget, self).__init__(**kwargs)
        def update_glsl(self, nap):
            ''' здесь меняем вершины и запускаем отрисовку '''
            Window.flip()
    
    class MainApp(App):
        def build(self):
            root = CustomWidget()
            EventLoop.ensure_window()
            return root
    
        def on_start(self):
            Clock.schedule_interval(self.root.update_glsl, 40 ** -1) #это наш желательный FPS
    
    if __name__ == '__main__':
        MainApp().run()

    Этот код уже покажет картинку (если добавить соответствующий функционал OpenGL), но картинка будет мерзко мигать. Это происходит потому, что у объекта Window есть свой вызов функции flip(), который вставляет чёрный экран в наш вывод. Этот вызов описан в упомянутом классе WindowBase. Добавим следующий код, отключающий лишний флип, в описание класса MainApp:

    def passFunc(W):
        pass
    
    class MainApp(App):
        def build(self):
            root = CustomWidget()
            EventLoop.ensure_window()
            
            #вот тут отключаем лишний флип:
            Window.on_flip = lambda W = Window: passFunc(W)
            
            return root

    Что дальше? После заполнения пробелов в этом коде, между программистом и apk-файлом остаётся только билдозер. Вот мануал, которого достаточно даже для непродвинутого юзера вроде меня (см. также подпись к видео, там есть скрипт, сильно упрощающий работу):

    Кроме того в spec-файле рекомендую включить полноэкранный режим для пущего фпс.

    Example вот. Код рисует инопланетянина с логотипа андроида (как на КДПВ), позволяет его вращать с помощью мышки/сенсора.

    А что там всё-таки с производительностью? Ну такое. Написал небольшую игрушку - змейку в трехмерном пространстве. FPS на самсунг А51 болтается между 15 и 25. Видео записано с телефона:

    Сын, между тем, отказался играться в моё поделие, ну хотя бы смотрит с интересом. Плоские рисованные игры двухлетке, оказывается, лучше заходят. Вот ради чего всё начиналось:

    Стиль рубленой змеи - пара скриншотов с этапа отладки как дань уважения Джеки Чану:
    Авангард 1
    Авангард 1
    Авангард 2
    Авангард 2
    При отладке текстур возникает желание использовать настроечную таблицу от старого телевещания
    При отладке текстур возникает желание использовать настроечную таблицу от старого телевещания

    Комментарии 2

      0
      Для игр на Kivy вам, я думаю, стоило посмотреть в сторону kivent. Ну, и конечно, все зависит от вашей реализации. В видео ниже, я не вижу проблем с производительностью:



      Хотя нужно признать, Kivy, не очень подходит для 3D игр.
        0
        Спасибо за наводку.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое