Калейдоскоп как в детстве


    Иногда отражение в зеркале более реально, чем сам объект…
    — Льюис Кэрролл (Алиса в зазеркалье)

    В юном возрасте у меня была забавная игрушка – калейдоскоп. Часами я рассматривал правильные узоры, составленные из разноцветных осколков битого стекла. Что-то завораживающее было в этом медитативном созерцании. Ныне, будучи отцом, мне захотелось показать своим чадам красоту правильных построений хаоса.

    Дети сейчас модерновые, им обычные игрушки малоинтересны, им компьютер подавай или планшет. Поэтому мне захотелось воссоздать цифровой прототип варианта калейдоскопа, а заодно по практиковать свои навыки работы с компьютерной графикой.

    Приглашаю и Вас окунуться со мной в мир отражений.

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

    Самым очевидным решением для меня стало использовать трассировку лучей (Ray tracing). Были созданы 3 зеркальные плоскости под углом 120 градусов друг к другу.



    Размещая объекты за дальним краем зеркал и использовав множественное переотражение лучей (около 20 отражений) получаем вполне себе рабочий калейдоскоп.



    Для создания рейтресинга используется вычислительный шейдер. Вывод изображения производится в текстуру, которая позже выводится на экран. В качестве объектов отрисовки используются сферы, как более простые фигуры. На моей видеокарте в режиме реалтайм рендеренга мне удалось добиться около 20-25 FPS, и это всего при трёх объектах и одном источнике света, что грустно. Хотелось хаотичного перемещения множества разнообразных фигур, как и источников освещения в реальном времени, но это привело бы к ещё большему замедлению.

    После нескольких подходов к оптимизации я отложил эту модель как малоперспективную.

    Код вычислительного шейдера GLSL
    #version 430 core
    layout( local_size_x = 32, local_size_y = 32 ) in;
    layout(binding = 0, rgba8) uniform image2D IMG;
    layout(binding = 1, std430) buffer InSphere {vec4 Shape_obj[];};
    layout(binding = 2, std430) buffer InSphere_color {vec4 Sphere_color[];};

    uniform vec2 u_InvScreenSize;
    uniform float u_ScreenRatio;
    uniform vec3 u_LightPosition;
    uniform vec3 u_CameraPosition;

    // задаём положение камеры четырьмя векторами
    const vec3 ray00 = vec3(-1*u_ScreenRatio,-1, -1.2);
    const vec3 ray01 = vec3(-1*u_ScreenRatio,+1, -1.2);
    const vec3 ray10 = vec3(+1*u_ScreenRatio,-1, -1.2);
    const vec3 ray11 = vec3(+1*u_ScreenRatio,+1, -1.2);
    const ivec2 size = imageSize(IMG);

    const mat3 mat_rotate = mat3(-0.5, -0.86602540378443864676372317075294, 0, 0.86602540378443864676372317075294, -0.5, 0, 0, 0, 1);
    struct plane {
    vec3 v_plane;
    vec3 n_plane;
    vec3 p_plane;
    };

    // объявляем три плоскости зеркала
    plane m[3];
    int last_plane;

    //----------------------------------------------------------
    float ray_intersect_sphere(vec3 orig, vec3 dir, vec4 Shape_obj) {
    vec3 l = Shape_obj.xyz - orig;
    float tca = dot(l,dir);
    float d2 = dot(l,l) - tca * tca;
    if (d2 > Shape_obj.w * Shape_obj.w) {return 0;}
    float thc = sqrt(Shape_obj.w * Shape_obj.w - d2);
    float t0 = tca - thc;
    float t1 = tca + thc;
    if (t0 < 0) {t0 = t1;}
    if (t0 < 0) {return 0;}
    return t0;
    }
    //---------------------------------------------------------
    'float ray_intersect_plane(in vec3 orig, in vec3 dir, inout plane p) {
    vec3 tested_direction = p.v_plane - orig;
    float k = dot(tested_direction, p.v_plane) / dot(dir, p.v_plane);
    if (k>=0) {
    vec3 p0 = orig + dir * k;
    // обрезаем зеркала в плоскости z
    if ((p0.z>-80)&&(p0.z<3)) {
    p.p_plane = p0;
    return length(p0-orig);
    }
    }
    return 1000000;
    }'+
    //---------------------------------------------------------
    bool all_obj(inout vec3 loc_eye, inout vec3 dir, inout vec3 c) {
    float min_len = 1000000;
    uint near_id = 0;
    float len;
    float min_len2 = 1000000;
    int near_id2 = -1;
    for (int i=0; i<3; i++) {
    if (i!=last_plane) {
    len = ray_intersect_plane(loc_eye, dir, m[i]);
    if (len<min_len2) {
    min_len2 = len;
    near_id2 = i;
    }
    }
    }

    // луч попал в одно из зеркал
    if (near_id2>=0) {
    loc_eye = m[near_id2].p_plane;
    dir = reflect(dir, m[near_id2].n_plane);
    last_plane =near_id2;
    return true;
    }

    for (uint i=0; i<Shape_obj.length(); i++) {
    len = ray_intersect_sphere(loc_eye, dir, Shape_obj[i]);
    if ((len>0)&&(len<min_len)) {
    min_len = len;
    near_id = i;
    }
    }
    // нет точки пересечения с объектами
    if (min_len>=1000000) {return false;}

    vec3 hit = loc_eye + dir * min_len;
    vec3 Normal = normalize(hit - Shape_obj[near_id].xyz);
    vec3 to_light = u_LightPosition - hit;
    float to_light_len = length(to_light);
    vec3 light_dir = normalize(to_light);
    float diffuse_light = max(dot(light_dir, Normal), 0.0);
    c = min(c + Sphere_color[near_id].xyz * (diffuse_light*0.8+0.2),1);
    return false;
    }
    //---------------------------------------------------------
    void main(void) {
    if (gl_GlobalInvocationID.x >= size.x || gl_GlobalInvocationID.y >= size.y) return;
    const vec2 pos = gl_GlobalInvocationID.xy * u_InvScreenSize.xy;
    vec3 dir = normalize(mix(mix(ray00, ray01, pos.y), mix(ray10, ray11, pos.y), pos.x));
    vec3 c = vec3(0, 0, 0);
    // начальная позиция камеры
    vec3 eye = vec3(u_CameraPosition);

    // задаём положение зеркалам
    m[0].v_plane = vec3(0,-5,0);
    m[0].n_plane = vec3(0,1,0);
    m[1].v_plane = mat_rotate * m[0].v_plane;
    m[1].n_plane = mat_rotate * m[0].n_plane;
    m[2].v_plane = mat_rotate * m[1].v_plane;
    m[2].n_plane = mat_rotate * m[1].n_plane;

    // максимальное число переотражений луча между зеркалами
    for (int i=0; i<20; i++) {
    if (!all_obj(eye, dir, c)) {break;}
    }

    // сохраняем текущий пиксель в текстуру
    imageStore(IMG, ivec2(gl_GlobalInvocationID.xy), vec4(c,1));
    }


    В ином подходе я использовал свойство периодичности узора калейдоскопа. Каждая вершина всегда связана с двумя другими, тут три вершины обозначены тремя цветами.
    Буферный объект мы заполняем координатами вершин равносторонних треугольников, образующих ромб.



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



    Далее заменяем цвета на текстурные координаты из мини-текстуры — шаблона.



    Пример заполнения текстуры прямоугольниками случайных цветов.

    Для улучшения отображения, шестигранник увеличиваем до размера экрана, а так же добавляем осевое вращение.

    После пары минут созерцания от вращения в одну сторону начало мутить. Для устранения данного неприятного эффекта, вращение было реализовано последовательно в каждую сторону.

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

    В итоге получились довольно симпатичные изображения







    Видео
    (Не мастак ваять видео, извиняюсь за качество)










    Код шейдерной программы невероятно прост.

    Код шейдеров GLSL
    //Вершинный шейдер
    #version 330 core
    layout (location = 0) in vec4 a_Position;
    uniform mat4 u_MVP;
    out vec4 v_Color;
    out vec2 v_TexCoords;
    void main() {
      v_TexCoords = a_Position.zw;
      gl_Position = u_MVP * vec4(a_Position.xy, 0, 1);
    }
    
    //Фрагментный шейдер
    #version 330 core
    precision mediump float;
    varying vec2 v_TexCoords;
    uniform sampler2D u_Texture;
    void main(){
      gl_FragColor = texture(u_Texture, v_TexCoords);
    }


    Дети остались довольны, а я завис в медитации на несколько вечеров.

    → Демо (EXE для Windows)

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      Дети сейчас модерновые, им обычные игрушки малоинтересны, им компьютер подавай или планшет.

      Дети не сами по себе модерновые, мы их такими делаем:
      Поэтому мне захотелось воссоздать цифровой прототип варианта калейдоскопа

      А ведь у калейдоскопа всё очарование именно в том, что он аналоговый…
        0
        На счёт детей, возможно они в первую очередь перенимают ценности родителей.
        А по поводу аналоговости, не соглашусь, думаю тут очарование всё же в геометрии.
          0
          Недавно вытащил калейдоскоп, дал детям. Комментарий старшего — ничего же там цветопередача, как достигли?
          0
          Отдельное спасибо за то, что на XP программа тоже работает :)
          Немного непонятно, зачем при изменении размера окна картинка масштабируется.
          Мне кажется, было бы наоборот интереснее отсекать часть картинки и дать возможность оставить маленькое окошко в виде «глазка» в классический калейдоскоп.
            0
            Не проверял, но возможно программа будет работать и на более старых версиях Виндовс, главное наличие видеокарты и драйверов.
            Собирал и под linux, но к сожалению не разобрался с настройками видеокарты, так что программа падала.
            Друзья предложили портировать проект на WebGl+JavaScript в окно браузера. Если общественность заинтересует, то это не так уж и сложно сделать.

            Что не так с масштабом, если я правильно понял нужна простая функция зуммирования картинки?
              0
              Зуммирование, наоборот, есть по умолчанию, масштаб меняется вместе с окном.
              А хотелось бы неизменения масштаба при изменении границ окна.
            0
            Рассматривался ли вариант использовать Ray tracing только для создания треугольного изображения (например для случайно расположенных полупрозрачных многогранниках) с последующим копированием? Для аутентичности можно было бы добавить затенение/размывание у крайних треугольников.
              0
              Тогда от рейтресинга смысла мало, вычислительно дешевле заранее рендерить фигуры в текстуру и двигать уже их с использованием альфа смешивания.
              0
              конечно мутит — с такой скоростью калейдоскоп не вращают )))
              Если в общем — получилось красиво. Ещё бы помедленнее менять картинку и вращать — было бы значительно лучше!
                0
                Делал на свой вкус. Однако вы правы, стоило бы немного сбавить обороты.
                Или лучше задавать опционально настройками, учту на будущее.
                +1
                С самого начала статьи вылезла мысль, что для калейдоскопа главное – всё же набор битых стекляшек и возможность их покрутить.
                Я думаю, что если развивать эту мысль – нужно сделать генерацию выпуклых полупрозрачных осколков и ёмкость, в которой они будут крутиться. И затем рендерить в текстуру, брать оттуда треугольник, а уже дальше шейдером выводить на экран то, что получилось. На том же Unity должно быть несложно реализовать, включая всю необходимую для калейдоскопа функциональность – вращение в обе стороны и несколько видов встряски. А дальше уже как фантазия пойдёт.
                Соотвественно, собрать можно будет для windows, linux, mac, ios, android – как минимум. На телефонах должно быть несложно привязаться к сенсорам, чтобы вращать ёмкость с виртуальным стеклом при вращении телефона. Ну и на webgl должно собраться, тогда и в браузере можно будет покрутить.
                  0
                  Вы знаете, а мне Ваша идея нравиться! Трассировать выпуклые объекты с прозрачностью в мини-текстуру во вне экранный буфер, а затем размножать треугольниками.
                    0
                    Зачем трассировать? Не обязательно. Конечно, трассировка может дать более реалистичное и красивое изображение, но в реальном времени тот же 1920×1080 с хотя бы 30 fps вряд ли даст. Тем более на мобильных устройствах. Достаточно просто использовать рендер Unity, прозрачные объекты он вроде сам рассортирует по расстоянию от камеры, так что «осколки» отрисуются в нужном порядке. Можно поставить две камеры – первая рисует в render texture осколки, вторая растягивает на весь экран простой прямоугольник, применяя аналог Вашего шейдера из статьи (только переписать с glsl на hlsl).
                    Ну и прелесть использования Unity ещё и в том, что физикой движения «осколков» он будет заниматься сам. Не нужно привязывать какой-нибудь Bullet, и связывать физику с отрисовкой. Можно прямо в runtime генерировать Mesh, и его использовать и с MeshFilter+MeshRenderer для отрисовки, и с MeshCollider+Rigidbody для физики.
                      0
                      Кстати, если всё же тянет на realtime трассировку, то в свежих версиях Unity можно более-менее просто организовать многопоточную трассировку с помощью ECS. Сначала в буфер забивать треугольники, затем W×H (если подходить к задаче «в лоб») подзаданий трассировки, причём выполняться будут параллельно, если не ошибаюсь, в N-2 потока (где N – количество логических ядер на текущем устройстве). Ну и затем переносить из буфера результат в текстуру.
                        0
                        Тут всё проще не нужно весь экран трассировать, достаточно небольшого изображения для окна в программе выше использовалось 64х64 пикселя, можно и меньше. Да и реальная физика осколков не нужна, достаточно правдоподобной эмуляции гравитации и вращения. Самое сложное на мой взгляд, создать сложные выпуклые многогранники не симметричной формы, так будет красивее.
                          0
                          Тут всё проще не нужно весь экран трассировать, достаточно небольшого изображения
                          О, и правда, чего это я.

                          Самое сложное на мой взгляд, создать сложные выпуклые многогранники не симметричной формы, так будет красивее.
                          А вот тут, наоборот, имхо всё не так сложно. Думаю, если генерировать настолько небольшую картинку, то форма осколков не имеет прямо очень большую важность, ведь там вряд ли будет много чего видно. Но я бы генерировал итеративно по принципу сборки модели гео-сферы из треугольников. Начинаем с четырёх вершин – правильного тетраэдра, затем на каждом шаге каждый треугольник разбиваем на три, добавляя новую вершину в центре треугольника. Новые вершины «нормализуем», отодвигая их от центра фигуры так, чтобы они лежали на радиусе сферы. Чем больше шагов, тем более ровный шар получится.
                          Вот только в данном случае нужен не шар, и чтобы фигура была не ровная и не симметричная. Для этого можно вносить правки в алгоритм, например:
                          • каждую сторону с вероятностью в 30% не преобразовывать;
                          • новую вершину создавать не в центре разбиваемого треугольника, а с случайной поправкой;
                          • начинать не только с тетраэдра, но и с куба, параллелепипеда;
                          • «добавлять шум», смещая каждую вершину в случайном направлении на небольшое расстояние, как до первой итерации, так и после последней (или даже перед каждой);
                          • с какой-то вероятностью после очередной итерации «складывать» одно из рёбер, заменяя две смежные вершины одной (в центре ребра, не забывая удалить вырожденные треугольники после этого).

                          В конце остаётся только проверить на выпуклость. «Забракованные» фигуры можно попробовать исправить, сдвигая самые дальние от центра точки ближе к центру, а самые ближние – наоборот, отодвигая. Ну или просто отбрасывать и начинать генерацию с начала.
                          Естественно, в вариациях с кубом и параллелепипедом нужно или работать с полигонами вместо треугольников, или первым делом заменять каждую плоскость на четыре треугольника, добавляя новую вершину в центре грани.
                          На мобильных устройствах можно делать меньше итераций, ну или сделать маленькую менюшку с настройками. Назвать «детализация» с пояснением, что чем она меньше – тем быстрее генерируется и отрисовывается, и наоборот.

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

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