Reflexão e refração impossíveis sem traçado recursivo de raios?

Estou escrevendo um renderizador de raytracing em tempo real baseado em GPU usando um shader de computação GLSL. Até agora, funciona muito bem, mas me deparei com um problema aparentemente insolúvel quando se trata de ter reflexões e refrações simultaneamente.

Minha lógica me diz que, para ter reflexões e refrações em um object, como o vidro, o raio teria que se dividir em dois, um raio refletirá da superfície e o outro será refletido pela superfície. As colors finais desses raios seriam então combinadas com base em alguma function e, em última instância, usadas como a cor do pixel do qual o raio se originou. O problema que tenho é que eu não posso dividir os raios em código de shader, como eu teria que usar a recursion para fazê-lo. Pelo que entendi, as funções em um shader não podem ser recursivas, porque todas as funções GLSL são como funções embutidas em C ++ devido a problemas de compatibilidade com hardware GPU mais antigo.

É possível simular ou falsificar a recursion no código do shader, ou posso até obter reflection e refração simultaneamente sem usar a recursion? Não vejo como isso pode acontecer sem recursion, mas posso estar errado.

Consigo converter back-raytracing em processo iterativo adequado para o GLSL com o método sugerido no meu comentário. Ele está longe de ser otimizado e eu não tenho todas as coisas físicas implementadas (nenhuma lei de Snell, etc …), mas como uma prova de conceito, ele já funciona. Eu faço todas as coisas em shader de fragment e código de lado de CPU há pouco envie as constantes de uniforms e cena em forma de textura de flutuador não-agarrada de 32 bit GL_LUMINANCE32F_ARB O processamento é só QUAD único que cobre canvas inteira.

  1. passando a cena

    Decidi armazenar a cena em textura para que cada raio / fragment tenha access direto a toda a cena. A textura é 2D, mas é usada como lista linear de floats de 32 bits. Eu decidi este formato:

     enum _fac_type_enum { _fac_triangles=0, // r,g,b,a, n, triangle count, { x0,y0,z0,x1,y1,z1,x2,y2,z2 } _fac_spheres, // r,g,b,a, n, sphere count, { x,y,z,r } }; const GLfloat _n_glass=1.561; const GLfloat _n_vacuum=1.0; GLfloat data[]= { // r, g, b, a, n, type,count 0.2,0.3,0.5,0.5,_n_glass,_fac_triangles, 4, // tetrahedron // px, py, pz, r, g, b -0.5,-0.5,+1.0, 0.0,+0.5,+1.0, +0.5,-0.5,+1.0, 0.0, 0.0,+0.5, -0.5,-0.5,+1.0, 0.0,+0.5,+1.0, 0.0, 0.0,+0.5, 0.0,+0.5,+1.0, +0.5,-0.5,+1.0, 0.0, 0.0,+0.5, +0.5,-0.5,+1.0, -0.5,-0.5,+1.0, }; 

    Você pode adicionar / alterar qualquer tipo de object. Este exemplo contém apenas um tetraedro azulado semitransparente. Você também pode adicionar matrizes de transformação mais coeficientes para as propriedades do material, etc …

  2. Arquitetura

    o sombreador Vertex apenas inicializa os cantos Raios da visão (posição inicial e direção) que é interpolada de forma que cada fragment represente o raio inicial do processo de traçado de raios traseiro.

Rastreamento de raio traseiro iterativo

Então criei uma lista “estática” de raios e iniciei com o raio inicial. A Iteração é feita em dois passos primeiro o traçado do raio traseiro:

  1. Faz um loop através de todos os raios em uma lista a partir do primeiro
  2. Encontre o cruzamento mais próximo com a cena …

    armazene a posição, propriedades normais de superfície e material em struct raio

  3. Se a interseção encontrada e não a última camada “recursiva” adicionar raios refletores / refratários para listar no final.

    também armazena seus índices para o struct processado

Agora seus raios devem conter todas as informações de interseção necessárias para reconstruir a cor. Fazer isso:

  1. loop através de todos os níveis de recursion para trás
  2. para cada um dos raios que combinam a camada de recursion real
  3. cor do raio de computação

    então use equações de iluminação que você quer. Se o raio contém crianças, adicione sua cor ao resultado com base nas propriedades do material (coeficientes refletivos e refrativos …)

Agora, o primeiro raio deve conter a cor que você deseja imprimir.

Uniformes usados:

tm_eye view camera matrix
aspect view ys / xs aspect ratio
n0 índice de refração do espaço vazio (não utilizado ainda)
comprimento focal da câmera focal_length
resolução fac_siz da textura quadrados cena
fac_num número de flutuadores realmente usados ​​na textura da cena
unidade de textura fac_txr para a textura da cena

Visualizar:

preview

O shader do fragment contém minhas impressões de debugging, então você também precisará da textura, se usada, veja o controle de qualidade:

  • Depuração de impressões GLSL

Façam:

adicione matrizes para objects, câmera etc.
adicionar propriedades do material (brilho, coeficiente de reflection / refração)
Lei de Snell agora a direção de novos raios está errada …
pode ser separado R, G, B para 3 começar raios e combinar no final
Falso espalhamento subsuperficial SSS com base em comprimentos de raio
melhor implementar luzes (agora elas são constantes em um código)
implementar mais primitivos (agora apenas triângulos são suportados)

[Editar1] código de debugging e atualização

Eu removi o código-fonte antigo para caber dentro do limite de 30KB. Se você precisar, desenterre-o no histórico de edições. Teve algum tempo para debugging mais avançada para isso e aqui o resultado:

preview

Esta versão resolveu alguns problemas geométricos, precisão, problemas de domínio e bugs. Eu tenho implementado reflexões e refrações, como é mostrado neste empate de debugging para ray teste:

visualização de depuração

Na visualização de debugging, apenas o cubo é transparente e o último raio que não atinge nada é ignorado. Então, como você pode ver a divisão de raios … O raio terminou dentro do cubo devido ao ângulo de reflection total E eu desabilito todos os reflexos dentro dos objects por razões de velocidade.

Os floats 32 bits para detecção de interseção são um pouco ruidosos com distâncias, portanto você pode usar doubles 64 bits, mas a velocidade cai consideravelmente nesse caso. Outra opção é rewrite a equação para usar coordenadas relativas que são mais precisas neste caso de uso.

Aqui a fonte dos shaders de float :

Vértice:

 //------------------------------------------------------------------ #version 420 core //------------------------------------------------------------------ uniform float aspect; uniform float focal_length; uniform mat4x4 tm_eye; layout(location=0) in vec2 pos; out smooth vec2 txt_pos; // frag position on screen < -1,+1> for debug prints out smooth vec3 ray_pos; // ray start position out smooth vec3 ray_dir; // ray start direction //------------------------------------------------------------------ void main(void) { vec4 p; txt_pos=pos; // perspective projection p=tm_eye*vec4(pos.x/aspect,pos.y,0.0,1.0); ray_pos=p.xyz; p-=tm_eye*vec4(0.0,0.0,-focal_length,1.0); ray_dir=normalize(p.xyz); gl_Position=vec4(pos,0.0,1.0); } //------------------------------------------------------------------ 

Fragmento:

 //------------------------------------------------------------------ #version 420 core //------------------------------------------------------------------ // Ray tracer ver: 1.000 //------------------------------------------------------------------ in smooth vec3 ray_pos; // ray start position in smooth vec3 ray_dir; // ray start direction uniform float n0; // refractive index of camera origin uniform int fac_siz; // square texture x,y resolution size uniform int fac_num; // number of valid floats in texture uniform sampler2D fac_txr; // scene mesh data texture out layout(location=0) vec4 frag_col; //--------------------------------------------------------------------------- //#define _debug_print #define _reflect #define _refract //--------------------------------------------------------------------------- #ifdef _debug_print in vec2 txt_pos; // frag screen position < -1,+1> uniform sampler2D txr_font; // ASCII 32x8 characters font texture unit uniform float txt_fxs,txt_fys; // font/screen resolution ratio const int _txtsiz=64; // text buffer size int txt[_txtsiz],txtsiz; // text buffer and its actual size vec4 txt_col=vec4(0.0,0.0,0.0,1.0); // color interface for txt_print() bool _txt_col=false; // is txt_col active? void txt_decimal(vec2 v); // print vec3 into txt void txt_decimal(vec3 v); // print vec3 into txt void txt_decimal(vec4 v); // print vec3 into txt void txt_decimal(float x); // print float x into txt void txt_decimal(int x); // print int x into txt void txt_print(float x0,float y0); // print txt at x0,y0 [chars] #endif //--------------------------------------------------------------------------- void main(void) { const vec3 light_dir=normalize(vec3(0.1,0.1,1.0)); const float light_iamb=0.1; // dot offset const float light_idir=0.5; // directional light amplitude const vec3 back_col=vec3(0.2,0.2,0.2); // background color const float _zero=1e-6; // to avoid intrsection with start point of ray const int _fac_triangles=0; // r,g,b, refl,refr,n, type, triangle count, { x0,y0,z0,x1,y1,z1,x2,y2,z2 } const int _fac_spheres =1; // r,g,b, refl,refr,n, type, sphere count, { x,y,z,r } // ray scene intersection struct _ray { vec3 pos,dir,nor; vec3 col; float refl,refr;// reflection,refraction intensity coeficients float n0,n1,l; // refaction index (start,end) , ray length int lvl,i0,i1; // recursion level, reflect, refract }; const int _lvls=5; const int _rays=(1< <_lvls)-1; _ray ray[_rays]; int rays; vec3 v0,v1,v2,pos; vec3 c,col; float refr,refl; float tt,t,n1,a; int i0,ii,num,id; // fac texture access vec2 st; int i,j; float ds=1.0/float(fac_siz-1); #define fac_get texture(fac_txr,st).r; st.s+=ds; i++; j++; if (j==fac_siz) { j=0; st.s=0.0; st.t+=ds; } // enque start ray ray[0].pos=ray_pos; ray[0].dir=normalize(ray_dir); ray[0].nor=vec3(0.0,0.0,0.0); ray[0].refl=0.0; ray[0].refr=0.0; ray[0].n0=n0; ray[0].n1=1.0; ray[0].l =0.0; ray[0].lvl=0; ray[0].i0=-1; ray[0].i1=-1; rays=1; // debug print area #ifdef _debug_print bool _dbg=false; float dbg_x0=45.0; float dbg_y0= 1.0; float dbg_xs=12.0; float dbg_ys=_rays+1.0; dbg_xs=40.0; dbg_ys=10; float x=0.5*(1.0+txt_pos.x)/txt_fxs; x-=dbg_x0; float y=0.5*(1.0-txt_pos.y)/txt_fys; y-=dbg_y0; // inside bbox? if ((x>=0.0)&&(x< =dbg_xs) &&(y>=0.0)&&(y< =dbg_ys)) { // prints on _dbg=true; // preset debug ray ray[0].pos=vec3(0.0,0.0,0.0)*2.5; ray[0].dir=vec3(0.0,0.0,1.0); } #endif // loop all enqued rays for (i0=0;i00;num--) { v0.x=fac_get; v0.y=fac_get; v0.z=fac_get; v1.x=fac_get; v1.y=fac_get; v1.z=fac_get; v2.x=fac_get; v2.y=fac_get; v2.z=fac_get; vec3 e1,e2,n,p,q,r; float t,u,v,det,idet; //compute ray triangle intersection e1=v1-v0; e2=v2-v0; // Calculate planes normal vector p=cross(ray[i0].dir,e2); det=dot(e1,p); // Ray is parallel to plane if (abs(det)<1e-8) continue; idet=1.0/det; r=ray[i0].pos-v0; u=dot(r,p)*idet; if ((u<0.0)||(u>1.0)) continue; q=cross(r,e1); v=dot(ray[i0].dir,q)*idet; if ((v<0.0)||(u+v>1.0)) continue; t=dot(e2,q)*idet; if ((t>_zero)&&((t< =tt)||(ii!=0))) { ii=0; tt=t; // store color,n ... ray[i0].col=c; ray[i0].refl=refl; ray[i0].refr=refr; // barycentric interpolate position t=1.0-uv; pos=(v0*t)+(v1*u)+(v2*v); // compute normal (store as dir for now) e1=v1-v0; e2=v2-v1; ray[i0].nor=cross(e1,e2); } } if (id==_fac_spheres) for (;num>0;num--) { float r; v0.x=fac_get; v0.y=fac_get; v0.z=fac_get; r=fac_get; // compute l0 length of ray(p0,dp) to intersection with sphere(v0,r) // where rr= r^-2 float aa,bb,cc,dd,l0,l1,rr; vec3 p0,dp; p0=ray[i0].pos-v0; // set sphere center to (0,0,0) dp=ray[i0].dir; rr = 1.0/(r*r); aa=2.0*rr*dot(dp,dp); bb=2.0*rr*dot(p0,dp); cc= rr*dot(p0,p0)-1.0; dd=((bb*bb)-(2.0*aa*cc)); if (dd<0.0) continue; dd=sqrt(dd); l0=(-bb+dd)/aa; l1=(-bb-dd)/aa; if (l0<0.0) l0=l1; if (l1<0.0) l1=l0; t=min(l0,l1); if (t< =_zero) t=max(l0,l1); if ((t>_zero)&&((t< =tt)||(ii!=0))) { ii=0; tt=t; // store color,n ... ray[i0].col=c; ray[i0].refl=refl; ray[i0].refr=refr; // position,normal pos=ray[i0].pos+(ray[i0].dir*t); ray[i0].nor=pos-v0; } } } ray[i0].l=tt; ray[i0].nor=normalize(ray[i0].nor); // split ray from pos and ray[i0].nor if ((ii==0)&&(ray[i0].lvl<_lvls-1)) { t=dot(ray[i0].dir,ray[i0].nor); // reflect #ifdef _reflect if ((ray[i0].refl>_zero)&&(t<_zero )) // do not reflect inside objects { ray[i0].i0=rays; ray[rays]=ray[i0]; ray[rays].lvl++; ray[rays].i0=-1; ray[rays].i1=-1; ray[rays].pos=pos; ray[rays].dir=ray[rays].dir-(2.0*t*ray[rays].nor); ray[rays].n0=ray[i0].n0; ray[rays].n1=ray[i0].n0; rays++; } #endif // refract #ifdef _refract if (ray[i0].refr>_zero) { ray[i0].i1=rays; ray[rays]=ray[i0]; ray[rays].lvl++; ray[rays].i0=-1; ray[rays].i1=-1; ray[rays].pos=pos; t=dot(ray[i0].dir,ray[i0].nor); if (t>0.0) // exit object { ray[rays].n0=ray[i0].n0; ray[rays].n1=n0; v0=-ray[i0].nor; t=-t; } else{ // enter object ray[rays].n0=n1; ray[rays].n1=ray[i0].n0; ray[i0 ].n1=n1; v0=ray[i0].nor; } n1=ray[i0].n0/ray[i0].n1; tt=1.0-(n1*n1*(1.0-t*t)); if (tt>=0.0) { ray[rays].dir=(ray[i0].dir*n1)-(v0*((n1*t)+sqrt(tt))); rays++; } } #endif } else if (i0>0) // ignore last ray if nothing hit { ray[i0]=ray[rays-1]; rays--; i0--; } } // back track ray intersections and compute output color col // lvl is sorted ascending so backtrack from end for (i0=rays-1;i0>=0;i0--) { // directional + ambient light t=abs(dot(ray[i0].nor,light_dir)*light_idir)+light_iamb; t*=1.0-ray[i0].refl-ray[i0].refr; ray[i0].col.rgb*=t; // reflect ii=ray[i0].i0; if (ii>=0) ray[i0].col.rgb+=ray[ii].col.rgb*ray[i0].refl; // refract ii=ray[i0].i1; if (ii>=0) ray[i0].col.rgb+=ray[ii].col.rgb*ray[i0].refr; } col=ray[0].col; // debug prints #ifdef _debug_print /* if (_dbg) { txtsiz=0; txt_decimal(_lvls); txt[txtsiz]=' '; txtsiz++; txt_decimal(rays); txt[txtsiz]=' '; txtsiz++; txt_decimal(_rays); txt_print(dbg_x0,dbg_y0); for (ii=0;iifloat(txtsiz))||(y<0.0)||(y>1.0)) return; // get font texture position for target ASCII i=int(x); // char index in txt x-=float(i); i=txt[i]; x+=float(int(i&31)); y+=float(int(i>>5)); x/=32.0; y/=8.0; // offset in char texture txt_col=texture(txr_font,vec2(x,y)); _txt_col=true; } //--------------------------------------------------------------------------- #endif //--------------------------------------------------------------------------- 

O código não está otimizado, mas queria que a física funcionasse corretamente primeiro. Ainda não existem Fresnells implementados, mas os coeficientes refl,refr res do material são usados.

Além disso, você pode ignorar o material de impressão de debugging (eles são encapsulados por #define ).

Eu construo uma pequena class para a textura geométrica para que eu possa facilmente configurar objects de cena. Foi assim que a cena foi iniciada para a pré-visualização:

 ray.beg(); // rgb rfl rfr n ray.add_material(1.0,1.0,1.0,0.3,0.0,_n_glass); ray.add_box ( 0.0, 0.0, 6.0,9.0,9.0,0.1); ray.add_material(1.0,1.0,1.0,0.1,0.8,_n_glass); ray.add_sphere( 0.0, 0.0, 0.5,0.5); ray.add_material(1.0,0.1,0.1,0.3,0.0,_n_glass); ray.add_sphere( +2.0, 0.0, 2.0,0.5); ray.add_material(0.1,1.0,0.1,0.3,0.0,_n_glass); ray.add_box ( -2.0, 0.0, 2.0,0.5,0.5,0.5); ray.add_material(0.1,0.1,1.0,0.3,0.0,_n_glass); ray.add_tetrahedron ( 0.0, 0.0, 3.0, -1.0,-1.0, 4.0, +1.0,-1.0, 4.0, 0.0,+1.0, 4.0 ); ray.end(); 

É importante que as normais calculadas estejam voltadas para fora dos objects, porque isso é usado para detectar cruzamentos de objects dentro / fora do object.

PS

Se você está interessado aqui é o meu traçador volumétrico de raio traseiro 3D:

  • Como escrever melhor um mecanismo de voxel em C com o desempenho em mente