понедельник, 6 мая 2013 г.

Немного о Render-to-Texture + ThermalVision Effect

Доброго, всем!
   
Если по простому, то Render-to-texture это технология OpenGL позволяющая рендерить содержимое экрана в специальную текстуру (не на экран)  а уже потом на экран. Возможно кто-то со мной не согласится, но выглядит это именно так. Что же это нам дает? Ответ на этот вопрос отчасти уже виден в предыдущих сообщениях: RippleEffect, MetaBlob. В примере RippleEffect шейдер применяется ко всем спрайтам находящимся на сцене, в примере MetaBlob рисуется геометрический шейдер также на всю сцену. Итак, по порядку.
Если вы внимательно разобрали приведенные выше примеры, то заметили, что для отображения картинки используется "специальный" спрайт. Именно, этот спрайт отвечает за отрисовку содержимого текущего фрейма. Нафиг он нам нужен? Очень нужен. =) Представьте себе, что вы захотели наложить шейдер не на конкретный спрайт а на какую-то область  экрана (или на весь экран), например для того, чтобы все что находится в этой области экрана видоизменялось согласно шейдеру. На картинке это может выглядеть так:




Также, вам может понадобится наложить несколько шейдеров и здесь тоже на помощь нам придет технология Render-to-texture.
Andengine дает нам возможность использовать данную технологию при помощи специального класса RenderTexture. Как и с обычной текстурой нам надо ее сначала создать, а потом передать ей графику. Создание RenderTexture почти ничем не отличается от создания обычной текстуры, разница заключается в "заполнении" текстуры графикой.
Создание текстуры:

mRenderTexture = new RenderTexture(this.getTextureManager(), renderTextureWidth, renderTextureHeight, PixelFormat.RGBA_8888);
mRenderTexture.init(pGLState);

Привязываем к спрайту:

mRenderTextureSprite = new UncoloredSprite(0f, 0f, TextureRegionFactory.extractFromTexture(mRenderTexture), getVertexBufferObjectManager());

Заполнение текстуры осуществляется в onDraw того контейнера (Entity), содержимое которого мы хотим передать в эту саму текстуру, размеры текстуры задаются в соответствии с размерами этого контейнера.
Заполнение текстуры выполняется в onDraw контейнера:

mRenderTexture.begin(pGLState, false, true, Color.TRANSPARENT);
   super.onDrawFrame(pGLState);
}
mRenderTexture.end(pGLState);


Теперь внесем немного ясности. Обратите внимание, что для отрисовки текстуры используется UncoloredSprite, а не просто Sprite. UncoloredSprite отличается тем, что у него отключены цветовые свойства, что приводит к повышению производительности при отрисовке. На самом деле вы можете использовать и простой спрайт. Заполнение текстуры можно вызвать в методе onDraw любого контейнера-Entity (Sprite, Rectangle, Mesh, SpriteGroup), а если мы хотим юзать весь экран, то лучше это сделать в onDrawFrame самого движка. Понятно, что для того, чтобы нам залезть в отрисовку, нам необходимо ее перекрыть. Вот как это может выглядеть для всего экрана (взято из указанных выше примеров):


//перекрыли создание движка

    @Override
    public Engine onCreateEngine(final EngineOptions pEngineOptions) {
        return new LimitedFPSEngine(pEngineOptions, 120) {
          
//перекрыли метод onDrawFrame
    @Override
    public void onDrawFrame(GLState pGLState)
          throws InterruptedException {
//если текстура еще не создана, то создаем ее
        if (!mRenderTextureInitialized) {
           initRenderTexture(pGLState);
           mRenderTextureInitialized = true;
        }

//заполняем текстуру содержимым фрейма
        mRenderTexture.begin(pGLState, false, true, Color.TRANSPARENT);
       { 
          super.onDrawFrame(pGLState);
       }
       mRenderTexture.end(pGLState);
     
//рисуем спрайт с нашей заполненной текстурой     
       pGLState.pushProjectionGLMatrix();
       pGLState.orthoProjectionGLMatrixf(0, mCamera.getSurfaceWidth(), 0, mCamera.getSurfaceHeight(), -1, 1);
       {
          mRenderTextureSprite.onDraw(pGLState, mCamera);
       }
       pGLState.popProjectionGLMatrix();
    }
   
    // процедура инициализации текстуры       
    private void initRenderTexture(GLState pGLState) {
                  //здесь размеры текстуры задали на всю поверхность
        mRenderTexture = new RenderTexture(this.getTextureManager(),mCamera.getSurfaceWidth(), mCamera.getSurfaceHeight(), PixelFormat.RGBA_8888);
        mRenderTexture.init(pGLState);
                  //размер спрайта также будет на весь экран
        mRenderTextureSprite = new UncoloredSprite(mCamera.getSurfaceWidth()/2, mCamera.getSurfaceHeight()/2, TextureRegionFactory.extractFromTexture(mRenderTexture), getVertexBufferObjectManager());
        //установили шейдер на спрайт  
        mRenderTextureSprite.setShaderProgram(ShockwaveShaderProgram.getInstance());    
    }
   };
 }



Для отдельно взятого спрайта нам надо перекрыть его метод onDraw.

Не трудно догадаться, что если нам понадобиться больше одного шейдера, то нам необходимы две RenderTexure и два спрайта через которые мы будем их отображать. Пример такой реализации под AndEngine GLES2 (не AnchorCenter! но адаптировать легко! вапще не сложно! ура!) лежит здесь.

На последок пара советов:
1. Не злоупотребляйте количеством шейдеров на один спрайт, мощности мобильных ГПУ пока что слабоваты. Постарайтесь подобрать оптимальный вариант эффекта с одни шейдером.
2. ФПС приложения напрямую зависит от размера экрана. Например, на HTC One S наш FrostedGlass идет без проблем, а на планшете Galaxy Tab (не смотря на всю его мощность) этот шейдер заметно притормаживает.
3. Не перекрывайте метод onDrawFrame движка в коде основной активити. Создайте для этого отдельный класс-наследник и обеспечьте его методами установки/снятия шейдеров по мере необходимости. Это поможет избежать трудностей с чтением кода и позволит сделать алгоритм вашей игры более гибким.
4. Если шейдер отлично идет на вашем  устройстве, это не означает что он пойдет на другом. Тестируйте на максимально возможном количестве девайсов.
5. Хотя с шейдером можно сделать то, что нельзя классической анимацией, не думайте, что разгрузив основной процессор отображением графики, вы получите качественный прирост производительности. Это не так. В случае мобильных устройств прироста fps я не наблюдал + батарея умирала не попрощавшись. Тем не менее использовать шейдера нужно, только не злоупотреблять ими, и дать возможность отключения.

Вот вроде бы и все, что я хотел рассказать. На десерт состряпал небольшой пример с комментариями. В нем, постарался максимально упростить понимание работы с RenderTexture и шейдерами в целом, привел пример наследника класса Engine, а также показал код реализованного шейдера ThermalVision содранного из лаборатории небезызвестного Geeks3D.com.
проект на гитхабе: https://github.com/unregistered33/ShaderTest.git


Комментариев нет:

Отправить комментарий