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

в 16:55, , рубрики: OpenGL, shaders, WebGL, графический дизайн, Калейдоскоп, Работа с 3D-графикой

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

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

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

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

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

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

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

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

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

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

Для создания рейтресинга используется вычислительный шейдер. Вывод изображения производится в текстуру, которая позже выводится на экран. В качестве объектов отрисовки используются сферы, как более простые фигуры. На моей видеокарте в режиме реалтайм рендеренга мне удалось добиться около 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));
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Видео

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

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

Код шейдеров 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)

Автор: Владислав

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js