Reflexión y refracción imposible sin trazado de rayos recursivo?

Estoy escribiendo un renderizador de trazado de rayos en tiempo real basado en GPU usando un sombreador de cálculo GLSL. Hasta ahora, funciona muy bien, pero me he topado con un problema aparentemente irresoluble cuando se trata de tener reflexiones y refracciones simultáneamente.

Mi lógica me dice que para tener reflexiones y refracciones sobre un objeto, como el vidrio, el rayo debería dividirse en dos, un rayo se refleja en la superficie y el otro se refracta a través de la superficie. Los colores finales de estos rayos se combinarían en función de alguna función y, finalmente, se usarían como el color del píxel del que se originó el rayo. El problema que tengo es que no puedo dividir los rayos en el código de sombreado, ya que tendría que usar recursividad para hacerlo. Desde mi punto de vista, las funciones en un sombreador no pueden ser recursivas porque todas las funciones GLSL son como funciones en línea en C ++ debido a problemas de compatibilidad con hardware GPU anterior.

¿Es posible simular o falsificar la recursión en el código del sombreador, o incluso puedo lograr la reflexión y la refracción simultáneamente sin usar la recursividad? No puedo ver cómo puede suceder sin recurrencia, pero podría estar equivocado.

Me las arreglo para convertir back-raytracing en un proceso iterativo adecuado para GLSL con el método sugerido en mi comentario. Está lejos de ser optimizado y no tengo todas las cosas físicas implementadas (ninguna ley de Snell, etc …) pero como una prueba de concepto ya funciona. Hago todas las cosas en el sombreador de fragmentos y el código del lado de la CPU solo envían las constantes de los uniforms y la escena en forma de textura flotante no sujeta de 32 bits GL_LUMINANCE32F_ARB El renderizado es solo QUAD cubre toda la pantalla.

  1. pasando la escena

    Decidí almacenar la escena en textura para que cada rayo / fragmento tenga acceso directo a toda la escena. La textura es 2D pero se usa como lista lineal de flotantes de 32 bits. Decidí 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, }; 

    Puede agregar / cambiar cualquier tipo de objeto. Este ejemplo contiene solo un solo tetraedro azulado semi transparente. También podría agregar matrices de transformación más coeficientes para las propiedades del material, etc.

  2. Arquitectura

    el sombreador Vertex acaba de inicializar los rayos de la esquina de la vista (posición de inicio y dirección) que se interpola para que cada fragmento represente el rayo de inicio del proceso de trazado del rayo de retroceso.

Trazo de rayo iterativo iterativo

Así que creé una lista “estática” de rayos e inicié con el rayo de inicio. La iteración se realiza en dos pasos primero, el rastreo de rayos hacia atrás:

  1. Pasa por todos los rayos en una lista de la primera
  2. Encuentra la intersección más cercana con la escena …

    almacena la posición, la superficie normal y las propiedades del material en la struct rayo

  3. Si se encuentra la intersección y no la última capa de “recursión”, agregue los rayos de reflexión / refracción a la lista al final.

    también almacenan sus índices a la struct rayo procesada

Ahora sus rayos deben contener toda la información de intersección que necesita para reconstruir el color. Para hacer eso:

  1. recorrer todos los niveles de recursión al revés
  2. para cada uno de los rayos que coinciden con la capa de recursión real
  3. calcular el color del rayo

    así que usa las ecuaciones de iluminación que quieras. Si el rayo contiene niños, agregue su color al resultado en función de las propiedades del material (coeficientes de refracción y refracción …)

Ahora el primer rayo debe contener el color que desea imprimir.

Uniformes utilizados:

tm_eye view camera matrix
Vista de aspect ys / xs ratio
n0 índice de refracción del espacio vacío (aún no utilizado)
focal_length focal de la cámara focal_length
resolución fac_siz de la textura cuadrada de la escena
fac_num número de flotadores realmente usados ​​en la textura de la escena
unidad de textura fac_txr para la textura de la escena

Avance:

avance

El sombreador de fragmentos contiene mis impresiones de depuración por lo que necesitarás también la textura si la utilizas. Consulta el control de calidad:

  • GLSL depura impresiones

Que hacer:

agregar matrices para objetos, cámara, etc.
agregar propiedades del material (brillo, coeficiente de reflexión / refracción)
La ley de Snell en este momento la dirección de los nuevos rayos está mal …
pueden ser R, G, B a 3 rayos de inicio separados y combinarse al final
falso SSS Despartwigmiento subsuperficial basado en longitudes de rayos
mejor implementar luces (ahora son constantes en un código)
implementar más primitivos (ahora solo se admiten triangularjs)

[Edit1] depuración de código y actualización

Eliminé el código fuente anterior para que quepa dentro de un límite de 30 KB. Si lo necesita, entóncelo del historial de edición. Tuvimos algo de tiempo para la depuración más avanzada para esto y aquí el resultado:

avance

esta versión resolvió algunos problemas geométricos, de precisión, de dominio y errores. Me implementaron las reflexiones y refracciones como se muestra en este dibujo de depuración para rayo de prueba:

vista de depuración

En la vista de depuración, solo el cubo es transparente y el último rayo que no golpea nada se ignora. Entonces, como pueden ver, el rayo se divide … El rayo termina dentro del cubo debido al ángulo de reflexión total. Y deshabilito todas las reflexiones dentro de los objetos por razones de velocidad.

Los floats 32 bits para la detección de intersecciones son un poco ruidosos con las distancias, por lo que puedes usar doubles 64 bits, pero la velocidad disminuye considerablemente en ese caso. Otra opción es reescribir la ecuación para usar coordenadas relativas que son más precisas en este caso de uso.

Aquí la fuente de sombreadores 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 //--------------------------------------------------------------------------- 

El código no está optimizado, pero quería que la física funcionara correctamente primero. Todavía no se implementa Fresnells, pero se utilizan refl,refr coeficientes de refl,refr de material en su lugar.

También puede ignorar las impresiones de depuración (están encapsuladas por #define ).

Construyo una clase pequeña para la textura geométrica para que pueda configurar fácilmente objetos de escena. Así es como se inició la escena para la vista previa:

 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(); 

Es importante que las normales calculadas estén orientadas hacia afuera de los objetos porque se usa para detectar cruces de objetos interiores / exteriores.

PD

Si está interesado aquí está mi trazador volumétrico de rayos 3D retroactivo:

  • Cómo escribir mejor un motor de vóxel en C con el rendimiento en mente