Сфера из двух треугольников

    История этой демки такова: однажды один мой друг сделал для своей игры генератор карт планет и захотел, чтобы созданные таким образом карты показывались в виде вращающейся сферы. Однако, при этом он не хотел использовать 3D-графику, а вместо этого сгенерировал множество кадров с этой самой сферой, повёрнутой на разные углы. Количество используемой памяти было… скажем так, избыточным, ну а скорость генерации кадров (как и качество их исполнения) сильно страдала. Чуть подумав, мне удалось помочь ему оптимизировать этот процесс, но в целом меня не покидало справедливое ощущение того, что это задача для OpenGL, а вовсе не для 2D-графики.

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

    И должен сказать, что у меня это получилось. Но обо всём по порядку.

    Математика процесса


    Для начала определимся с собственно задачей. Для каждой точки на экране у нас имеются две экранные координаты в декартовой системе координат, и нам необходимо найти для неё сферические координаты (фактически, широту и долготу), которые по сути и являются текстурными координатами для карты планеты.

    Итак. Переход от сферической системы координат к декартовой задаётся системой уравнений (взято с Википедии):


    а обратный переход — такими уравнениями:


    Координату Z мы легко можем получить из X и Y, зная радиус, а сам радиус мы можем принять равным единице.
    В дальнейшем договоримся о том, что приведённые выше уравнения мы слегка изменим, поменяв местами понятия Y (у нас это будет экранная вертикаль) и Z (это будет глубина сцены).

    Техническая часть


    Реализация идеи потребует от нас применения квада (я уже писал о том, как его использовать, поэтому повторяться не буду, тем более что ниже приведена ссылка на полный исходный код проекта), а также двух текстур: собственно карты планеты (я использовал текстуру Земли размера 2048x1024) и карты текстурных координат. Код генерации второй текстуры аккуратно повторяет математику преобразования из декартовых координат в сферические:

    int texSize = 1024;
    double r = texSize * 0.5;
    int[] pixels = new int[texSize * texSize];
    
    for (int row = 0, idx = 0; row < texSize; row++) {
        double y = (r - row) / r;
        double sin_theta = Math.sqrt(1 - y*y);
        double theta = Math.acos(y);
        long v = Math.round(255 * theta / Math.PI);
    
        for (int col = 0; col < texSize; col++) {
            double x = (r - col) / r;
            long u = 0, a = 0;
    
            if (x >= -sin_theta && x <= sin_theta) {
                double z = Math.sqrt(1 - y*y - x*x);
                double phi = Math.atan2(z, x);
                u = Math.round(255 * phi / (2 * Math.PI));
                a = Math.round(255 * z);
            }
    
            pixels[idx++] = (int) ((a << 24) + (v << 8) + u);
        }
    }
    
    GLES20.glGenTextures(1, genbuf, 0);
    offsetTex = genbuf[0];
    if (offsetTex != 0) {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, offsetTex);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_NONE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_NONE);
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, texSize, texSize, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, IntBuffer.wrap(pixels));
    }
    

    Отметим, что координаты X и Y переводятся из диапазона [0..texSize] в диапазон [-1..1], а текстурные координаты U и V переводятся из радианов в диапазон [0..255], после чего записываются соответственно в красную и зелёную компоненты 32-битной текстуры. Альфа-канал используется для сохранения «глубины» (координаты Z), а синий пока остаётся незадействованным. Отключение билинейной фильтрации также не случайно: на данном этапе она не даёт какого-либо эффекта (соседние точки в любом случае имеют одни и те же значения, с довольно резкими скачками), а в том, что я собираюсь показать дальше, она и вовсе будет вредна. Но об этом ниже.

    Обе текстуры подаются на вход простого пиксельного шейдера (здесь и далее картинки кликабельны):

    private final String quadFS =
        "precision mediump float;\n" +
        "uniform sampler2D uTexture0;\n" +
        "uniform sampler2D uTexture1;\n" +
        "varying vec4 TexCoord0;\n" +
        "void main() {\n" +
        "	vec4 vTex = texture2D(uTexture0, TexCoord0.xy);\n" +
        "	vec3 vCol = texture2D(uTexture1, vTex.xy).rgb;\n" +
        "  	gl_FragColor = vec4(vCol, (vTex.w > 0.0 ? 1.0 : 0.0));\n" +
        "}\n";
    

    Код отрисовки сцены я не привожу, т.к. в нём всё довольно тривиально (и, опять же, его можно посмотреть в полном исходнике), да и сам шейдер довольно примитивен. Самое любопытное в нём, пожалуй, то, что альфа-канал пока лишь проверяется на положительность, тогда как можно было бы задействовать его для эффекта освещения.

    Получилось уже довольно неплохо, однако как-то плоско, плюс хотелось бы добавить собственно вращение планеты вокруг своей оси.

    Включаем в шейдер ещё один параметр (будем менять его в зависимости от времени в диапазоне [0..1]), плюс добавляем «глубину» (умножение цвета на значение из альфа-канала):

    private final String quadFS =
        "precision mediump float;\n" +
        "uniform sampler2D uTexture0;\n" +
        "uniform sampler2D uTexture1;\n" +
        "uniform float uOffset;\n" +
        "varying vec4 TexCoord0;\n" +
        "void main() {\n" +
        "	vec4 vTex = texture2D(uTexture0, TexCoord0.xy);\n" +
        "	vTex.x += uOffset;\n" +
        "	vec3 vCol = texture2D(uTexture1, vTex.xy).rgb;\n" +
        "  	gl_FragColor = vec4(vCol * vTex.w, (vTex.w > 0.0 ? 1.0 : 0.0));\n" +
        "}\n";
    

    Что ж, к самой сфере претензий нет, однако картинка выглядит как-то… восьмибитно, что ли. И неудивительно: мы же записывали текстурные координаты в диапазоне [0..255] (максимум, доступный нам в обычных цветовых компонентах), а значит наша текстура может иметь не больше 256 точек в высоту (и 512 в ширину, учитывая вращение). Маловато, нужна как минимум 10-битная точность.

    Увеличиваем разрешение


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

    Итак, у нас пока задейстованы две из трёх цветовых компоненты, т.е. 16 бит из 24. Ну так упакуем же данные так, чтобы каждая текстурная координата имела размер 12 бит, что позволит нам работать с текстурами размером до 4096 пикселей в высоту! Для этого изменим буквально три строчки в программе:

    ...
    long v = Math.round(4095 * theta / Math.PI);
    ...
            u = Math.round(4095 * phi / (2 * Math.PI));
    ...
        pixels[idx++] = (int) ((a << 24) + (v << 12) + ((u & 15) << 8) + (u >> 4));
    ...
    

    и напишем новый шейдер, учитывающий 12-битную схему адресации (именно в этом месте необходимо, чтобы билинейная фильтрация была отключена!):

    private final String quadFS =
        "precision mediump float;\n" +
        "uniform sampler2D uTexture0;\n" +
        "uniform sampler2D uTexture1;\n" +
        "uniform float uOffset;\n" +
        "varying vec4 TexCoord0;\n" +
        "void main() {\n" +
        "	vec4 vTex = texture2D(uTexture0, TexCoord0.xy);\n" +
    
        "	vec3 vOff = vTex.xyz * 255.0 + vec3(0.5, 0.5, 0.5);\n" +
        "	float hiY = floor(vOff.y / 16.0);\n" +
        "	float loY = vOff.y - 16.0 * hiY;\n" +
        "	vec2 vCoord = vec2(\n" +
        "		(vOff.x * 16.0 + loY) / 4095.0 + uOffset,\n" +
        "		(vOff.z * 16.0 + hiY) / 4095.0);\n" +
    
        "	vec3 vCol = texture2D(uTexture1, vCoord).rgb;\n" +
        "  	gl_FragColor = vec4(vCol * vTex.w, (vTex.w > 0.0 ? 1.0 : 0.0));\n" +
        "}\n";
    

    Ну это же совсем другое дело! С небольшими изменениями (добавив масштабирование «щипком» и вращение пальцем) я эту программу показывал своим друзьям и колегам, и при этом спрашивал, сколько, по их мнению, в этой сцене треугольников. Результаты варьировались, да и сам вопрос вызывал подозрение в наличии подвоха (в этом случае респонденты шутили «один», что было недалеко от истины), но правильный ответ стабильно удивлял. И все, как один, спрашивали: а почему сферу можно крутить вокруг одной оси, но нельзя наклонять?.. Хм.

    Наклон


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

    В сущности, задача сводится к тому, чтобы взять смещённую координату V, тогда как координата U не меняется: это происходит потому, что мы добавляем вращение вокруг оси X. План такой: преобразуем текстурные координаты в экранные (в диапазоне [-1..1]), применяем к ним матрицу поворота вокруг горизонтальной оси (для этого заранее запишем в новую константу uTilt синус и косинус угла наклона), а дальше воспользуемся новой координатой Y для выборки в нашей шаблонной текстуре. «Повёрнутая» координата Z нам тоже пригодится, с её помощью мы отзеркалим долготу для обратной стороны шарика). Экранную координату Z придётся посчитать явно, чтобы не делать две текстурных выборки из одной текстуры, заодно это повысит её точность.

    private final String quadFS =
        "precision mediump float;\n" +
        "uniform sampler2D uTexture0;\n" +
        "uniform sampler2D uTexture1;\n" +
        "uniform float uOffset;\n" +
        "uniform vec2 uTilt;\n" +
        "varying vec4 TexCoord0;\n" +
        "void main() {\n" +
        "	float sx = 2.0 * TexCoord0.x - 1.0;\n" +
        "	float sy = 2.0 * TexCoord0.y - 1.0;\n" +
        "	float z2 = 1.0 - sx * sx - sy * sy;\n" +
    
        "	if (z2 > 0.0) {;\n" +
        "		float sz = sqrt(z2);\n" +
        "		float y = (sy * uTilt.y - sz * uTilt.x + 1.0) * 0.5;\n" +
        "		float z = (sy * uTilt.x + sz * uTilt.y);\n" +
    
        "		vec4 vTex = texture2D(uTexture0, vec2(TexCoord0.x, y));\n" +
        "		vec3 vOff = vTex.xyz * 255.0 + vec3(0.5, 0.5, 0.5);\n" +
        "		float hiY = floor(vOff.y / 16.0);\n" +
        "		float loY = vOff.y - 16.0 * hiY;\n" +
        "		vec2 vCoord = vec2(\n" +
        "			(vOff.x * 16.0 + loY) / 4095.0,\n" +
        "			(vOff.z * 16.0 + hiY) / 4095.0);\n" +
    
        "		if (z < 0.0) { vCoord.x = 1.0 - vCoord.x; }\n" +
        "		vCoord.x += uOffset;\n" +
    
        "		vec3 vCol = texture2D(uTexture1, vCoord).rgb;\n" +
        "   	gl_FragColor = vec4(vCol * sz, 1.0);\n" +
        "	} else {\n" +
        "   	gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);\n" +
        "	}\n" +
        "}\n";
    

    Ура, наклон удался! Вот только странный шум на границе полушарий немного смущает. Очевидно, проблема кроется в недостаточной точности адресации в граничных точках (точки на самой окружности соответствуют слишком большому диапазону координат, один тексель расползается на интервал довольно заметной длины). В конце концов, мне удалось это победить, используя две сгенерированных текстуры вместо одной.

    В итоге, можно приближать и крутить шарик почти так же, как в Google Earth. С тем отличием, что здесь — всего-навсего два треугольника.

    Ну и, наконец, обещанное. Исходный код проекта доступен на GitHub.
    Также можно скачать готовый .apk-файл.

    Кстати, исходники для моих прошлых постов доступны там же.

    Update: Кажется, мне всё-таки удалось добиться точного текстурирования на всех устройствах, это потребовало немного изменить порядок битов, а также немного подкорректировать округление цветов шаблонной текстуры. Теперь даже насильно сжатые текстуры должны работать более-менее нормально. Код на GitHub обновлён, новые .apk-файлы залиты.

    Update 2: Всё-таки артефакт на границе полушарий был побеждён. Исходники и готовый .apk обновлены.
    Кроме того, я добавил ещё один бонус: WebGL-версия этой демки доступна здесь.
    Support the author
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 66

      +2
      На HTC One X получилось вот такое db.tt/2A2yKX5u
      И еще, у вас опечатка: первые уравнения у вас для преобразования из сферической в декартову, а вторые наоборот.
        0
        Хм, странно, мне казалось, что на One я тоже тестировал. Похоже, спутал с Butterfly или ещё чем-то. Значит, там такая же беда, как на S4. :(
          0
          Nexus 7, аналогично.
            0
            Чёрт. Спасибо за отзыв, сегодня постараюсь добить этот глюк.
            +4
            30 fps на третьей тегре с двумя треугольниками? Ну оок, лол.
              +1
              Ну тегра — это вообще странная вещь. На моём S3, например, стабильно держится 60 fps, а значит кадровая частота ограничена лишь VSync'ом.
                0
                Угу, очень странная, но Epic Citadel от Epic Games выдает средний фпс 57.
                  +2
                  Значит, там довольно примитивные пиксельные шейдеры, только и всего.
                    –1
                    Ну да, ну да…
                      0
                      Попробуйте вот этот файл (это вариант без наклона) — сколько на нём будет FPS?
                        0
                        60 фпс, проблема с текстурой. Может она пожата в неподдерживаемый формат?
                          0
                          А на предыдущем варианте проблемы не было?
              0
              А, я перепутал One и One X.
              Значит, проблема как минимум с теграми (One X и Nexus 7).
                0
                На WindowsAndroid сначала «Unfortunately, Planet has stopped.», потом пустое чёрное окно с заголовком.
                  0
                  Любопытно. Stopped оно в эмуляторе действительно иногда делало, но с этим, вроде, мне уже удалось справиться.
                  А чёрное окно обычно у меня длилось секунд 10, не больше. Я имею в виду, в эмуляторе: на устройстве-то всё грузится за секунду.
                    0
                    По-моему, для демок лучше использовать WebGL, а не Android.
                      0
                      Хм… Кажется, после этого комментария я таки решусь попробовать освоить WebGL.
                  0
                  Попробуйте обновлённую сборку, пожалуйста. Мне как раз случай с вашего скриншота был наиболее интересен, вдруг удалось его исправить?
                    0
                    Уже лучше, но артефакты есть. db.tt/FrXy9lO7
                    И по поводу некорректного отображения на стыке передней и задней полусфер: можно ведь преобразовывать координаты так, чтоб проекция с большим углом к сфере была всегда по периметру отображаемой плоской проекции.
                      0
                        0
                        Там тот же билд. :)
                        Вообще, странно, что даже для одного и того же чипа (Tegra 3) результаты получаются разными (на Nexus 7 всё ОК, на One X нет), но тут уже, видимо, какая-то разница в драйверах начинает сказываться. Если мне One X попадётся в руки, попытаюсь додавить этот эффект. Ну, или может быть кто-нибудь мне поможет, исходники-то открыты. :)
                        0
                        Хотя… Что-то я засомневался, тот же ли. Попробуйте, действительно, вот это: вдруг я в последний момент, как обычно, всё перепутал?..
                    +3
                    В свое время делал что-то похожее (еще на спектрум потом на 386х).
                    Вращающиеся фигуры назывался. (разные фигуры на выбор вращались на экране)
                    Типа наглядного пособия по математике.
                    Только там я применял массивы значений косинусов, чтобы быстрее считалось.
                    Писалось на Бейсике, в тетрадках. Потом по полдня вбивалось.
                    Была даже скомпилиная программа, которая зачетом ушла и больше не осталась.
                    Надо бы повторить, с оглядкой на Ваш код.
                    И кстати, просчет не видимости точки (что-то типа Z буфера) высчитывался как-то интересно.
                    Я потом эту формулу в другом виде в мат.анализе нашел. Хотя вывел сам.
                      +1
                      На Lenovo A750 всё работает, 30FPS.

                      А Земля у Вас в правильную сторону вращается? А то мне кажется что нет )
                        0
                        О, а вот об этом я ни разу не задумывался, спасибо. :)
                          0
                          Кстати, попробуйте вот этот файл (это вариант без наклона) — сколько на нём будет FPS?
                            0
                            Забавный результат…

                            50+ FPS но появились деформации как на скрине в первом коменте только гораздо меньшие. как буж-то землю нарезали на плоские круги и некоторые слегка провернули относительно других.
                              0
                              Ого, вот это странно. У меня сейчас такой эффект на One тоже воспроизводится (именно такой, «слоями», а не как в комментарии), но с чего бы он мог появиться, если предыдущий вариант работал нормально?.. Загадочно.
                                0
                                Выглядит забавно :) На вскидку — может где-то ошибки округления возникают ( всмысле погрешности вычисления координат)?
                                  0
                                  Ну, они бы и в предыдущем варианте тогда возникали. :) А вычисление текстурных координат в обоих шейдерах сделано одинаково.
                                  Вообще, вот сейчас я ковыряю One, и у меня стойкое ощущение, что он насильно сжимает текстуру. Если это так, то я уже ничего сделать не смогу…
                                    0
                                    Собрал ещё три варианта. :)
                                    На этот раз, у меня получилась почти нормальная работа на HTC One, путём переставления битов и изменения множителя. Мелкие артефакты изображения всё равно присутствуют, но стало уже лучше.
                                    Итак, для HTC One: версия "только вращение", версия с наклоном.
                                      0
                                      выглядит классно на nexus 7, 60fps, только где наклон выбают артефакты
                                        0
                                        Такие, как на последнем скриншоте? ;)
                                          0
                                          да, как думаете, справитесь с этим? я залип на этот шарик, на работе, минут на 15, еще коллегам потом показывал ) А можно еще звезды добавить на задний фон?
                                            0
                                            Хороший вопрос. С одной стороны, хочется это исправить, а с другой — шейдер «с наклоном» уже и так очень сложный, от изящества первой версии в нём почти ничего не осталось. Так-то демка создавалась чисто ради вращения вокруг своей оси.
                                            Так или иначе, исходники доступны всем, поэтому если не я, то кто-нибудь другой наверняка что-то сможет сделать. :)
                                            Ну а звёзды… чуть позже прикручу. :)
                                        0
                                        Тестирую эту версию, обе дают 60 фпс, версия с наклоном изначально без наклона, покрутил, всё ок, только на полюсах артефакты.
                                          0
                                          Вот сейчас совсем странно стало, почему тогда у BlackSwan первая версия работала нормально (а почти такая же перестала), а у вас она же давала 30 fps? Что же это был за билд такой?..
                                          Как бы то ни было, добавил в пост ссылки, чтобы можно было выбрать себе сборку по вкусу. :)
                                            0
                                            Сплошные загадки (:
                              +1
                              Вот так, имея один эмулятор Android, можно потестировать свое приложение «коллективным разумом» на большом количестве различных android девайсов
                                0
                                Evo 3D, 60-61 fps. Только при приближении появляется небольшой шум.
                                  0
                                  Попробуйте сборку «для HTC One», может она будет работать лучше?
                                    0
                                    Лучше, на первый взгляд.
                                    Но чуть-чуть шум остался
                                    image
                                      +1
                                      Ну и последний, пожалуй, билд на сегодня: вот. :)
                                        0
                                        Идеально :)
                                  0
                                  Читаю коменты, и мне страшно. Если два треугольника с шейдерами вызывают такие различия на разных девайсах, через что же проходят разработчики более серьезных игр…
                                    0
                                    В более серьёзных играх не используются такие жестокие хаки. :)
                                      0
                                      посмотри на шейдеры более-менее реалистичного освещения или сглаживания.
                                    0
                                    Спасибо за статью. Я подобное во флеше делал, тока до гениальности проще. Брал рисовал круг, под ним слой с прямоугольной картой+ маска по форме круга и двигал карту слева направо циклично. В итоге сделал солнечную систему из 7 планет. Задачка на экзамене такая была.
                                      0
                                      Вот да, мой друг изначально сделал примерно так же. Но при этом нет «выпуклости», перспективы, поэтому мне и захотелось сделать примерно так же просто, но более правдоподобно.
                                      0
                                      можно еще с нормалями побаловаться, придав земле эффект трехмерности.
                                      ru.wikipedia.org/wiki/Рельефное_текстурирование
                                        0
                                        Ну да, решив задачу нахождения сферических текстурных координат, дальше уже можно двигаться в направлении любых спецэффектов, использующих текстуры. :)
                                          0
                                          Я такое реализовывал
                                            0
                                            дальше советую посмотреть в сторону Clipmaps или мегатекстуры, для того чтоб увеличить качество при увеличении. получится не хуже GoogleEarth
                                          0
                                          А какая проекция используется в исходном изображении?
                                            0
                                            Судя по всему, простая цилиндрическая (она же простая географическая, она же equirectangular, она же plate carrée).
                                            0
                                            просто имхо в тему, может пригодится:
                                            http://www.ixbt.com/video/sphere-triangles.shtml
                                              +3
                                              Стал портировать на WebGL, получилась вот такая пангея.
                                                +1
                                                У меня получилось немного получше. :)
                                                  0
                                                  А почему только в Chrome работает?
                                                    0
                                                    У меня работает в Chrome и Firefox, но не работает в Safari.
                                                    Я пока не разбирался в причинах, всё-таки это мой первый webGL, сделанный менее чем за сутки.
                                                      +1
                                                      Ан нет, у меня в Safari тоже работает: там надо было явно включить поддержку WebGL, тогда всё пучком.
                                                      Больше скажу, теперь у меня это работает даже в Firefox for Android.
                                                  +1
                                                  Отличная статья!
                                                  И очень хорошая идея. Ваш код и «хак» arbg-формата отлично работает на IOS.
                                                  Попробовала сделать на IPhone, image

                                                  Одна проблема с fps. С чего ей так падать непонятно, может у кого есть мысли насчет этого?
                                                  Причем пробовала сделать нативные шрейдеры, которые сами делают преобразование из декартовых в сферические, но тоже грустно получилось с производительностью.
                                                    0
                                                    Зависит от версии айфона: например, на четвёрке это было бы ожидаемо, т.к. там впервые появилось ретина-разрешение, при незначительном увеличении мощности процессора (по сравнению с 3GS). Тут ведь производительность упирается именно в количество экранных пикселей и мощность видеоядра.
                                                    0
                                                    Да, на четверке смотрю. На ipad-mini ситуация получше. Но все-таки это довольно странно. Я работаю на cocos2d и там огромное множество преобразований со спрайтами, и fps не падает. А тут от одной картинки все так опустилось. В предложенном методе, вообще, исключительно линейные преобразования, которые GPU по сути должен на раз-два делать. Возможно тут переключение между текстурами играет нехорошую роль. Но за статью и за идею Вам спасибо, при любом fps).
                                                      0
                                                      Преобразования со спрайтами обычно делаются на уровне вершин, которых в спрайте всего четыре штуки (как и здесь). А вот пикселей здесь — примерно 321700 (это только те, которые попадают под «if (z2 > 0.0)», а так их 409600), и для каждого выполняется довольно сложный пиксельный шейдер с произвольной выборкой из двух текстур.
                                                      Так что, да, в данном случае это всего лишь просто забавная демка, с сомнительной применимостью, хотя позапрошлогодние GPU её уже вполне тянут на 60 fps, а дальше будет только лучше.
                                                        0
                                                        Насколько я поняла по анализу перфоманса, когда текстура читается непоследовательно, а зависимо от переменных происходит сильное замедление. Видимо в это и есть причина. Согласна, что для новых устройств это уже утрачивает актуальность.

                                                    Only users with full accounts can post comments. Log in, please.