Хабрахабр

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

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

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

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

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

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

Размещая объекты за дальним краем зеркал и использовав множественное переотражение лучей (около 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 ;
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;

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

5, -0. const mat3 mat_rotate = mat3(-0. 86602540378443864676372317075294, -0. 86602540378443864676372317075294, 0, 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;}

0);
c = min(c + Sphere_color[near_id].xyz * (diffuse_light*0. 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. 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); 8+0.

// задаём положение зеркалам
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)

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть