Caleidoscópio como na infância


Às vezes, o reflexo no espelho é mais real que o próprio objeto ...
- Lewis Carroll (Alice no espelho)

Em tenra idade, tive um brinquedo engraçado - um caleidoscópio. Durante horas, examinei os padrões corretos compostos por fragmentos multicoloridos de vidro quebrado. Algo fascinante estava nessa contemplação meditativa. Agora, como pai, queria mostrar aos meus filhos a beleza das construções corretas do caos.

As crianças agora são modernas, os brinquedos comuns são de pouco interesse para elas, dê-lhes um computador ou tablet. Portanto, eu queria recriar um protótipo digital de uma variante de um caleidoscópio e, ao mesmo tempo, praticar minhas habilidades em computação gráfica.

Convido você a mergulhar comigo no mundo das reflexões.

A idéia original era construir um modelo físico completo de um caleidoscópio. Este dispositivo consiste em vários espelhos localizados em ângulo entre si, colocados em um tubo. O caleidoscópio da minha infância consistia em três espelhos, e decidi recriar esse design.

A solução mais óbvia para mim foi usar o traçado de raios. Três planos de espelho foram criados em um ângulo de 120 graus entre si.



Colocando objetos além da borda mais distante dos espelhos e usando a reflexão múltipla dos raios (cerca de 20 reflexões), obtemos um caleidoscópio bastante funcional.



Para criar um roteamento, um shader computacional é usado. A imagem é impressa em uma textura, que é exibida mais tarde na tela. Esferas são usadas como objetos de desenho como formas mais simples. Na minha placa de vídeo no modo de renderização em tempo real, consegui atingir de 20 a 25 FPS, e isso é apenas com três objetos e uma fonte de luz, o que é triste. Eu queria um movimento caótico de muitas formas diferentes, bem como fontes de iluminação em tempo real, mas isso levaria a uma desaceleração ainda maior.

Após várias abordagens à otimização, adiei esse modelo como pouco promissor.

Código de sombreamento computacional 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));
}


Em outra abordagem, usei a propriedade de periodicidade do padrão caleidoscópio. Cada vértice está sempre conectado com outros dois, aqui três vértices são indicados por três cores.
Enchamos o objeto de buffer com as coordenadas dos vértices dos triângulos equilaterais formando um losango.



Na figura, as cores são substituídas por números. Observe: as linhas pares e ímpares são repetidas com um turno de um. Cortamos elementos desnecessários, exibindo apenas os índices de vértices necessários e, como resultado, obtemos um hexágono que pode ser facilmente escalado.



Em seguida, substitua as cores pelas coordenadas de textura do modelo de mini textura.



Um exemplo de preenchimento de uma textura com retângulos de cores aleatórias.

Para melhorar a exibição, aumente o hexágono para o tamanho da tela e adicione rotação axial.

Após alguns minutos de contemplação a partir da rotação em uma direção, começou a se agitar. Para eliminar esse efeito desagradável, a rotação foi implementada sequencialmente em cada direção.

Inicialmente, a textura era preenchida com elementos aleatórios, mas depois surgiu a idéia de usar imagens ou fotografias coloridas. O elemento de exibição passa pela imagem em uma direção aleatória na forma de uma janela deslizante, mudando periodicamente a direção. Portanto, o padrão é mais saturado e interessante.

O resultado são imagens muito legais







Vídeo
(Não sei como esculpir um vídeo, peço desculpas pela qualidade)










O código do sombreador é incrivelmente simples.

Código de sombreador 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); } 


As crianças ficaram satisfeitas e eu fiquei em meditação por várias noites.

Demo (EXE para Windows)

Source: https://habr.com/ru/post/pt461351/


All Articles