没有递归光线追踪,反射和折射不可能吗?

8
我正在使用GLSL计算着色器编写基于GPU的实时光线追踪渲染器。目前为止,它工作得非常好,但当涉及同时具有反射和折射时,我遇到了一个看似无法解决的问题。
我的逻辑告诉我,在对象(如玻璃)上具有反射和折射,光线必须分裂成两条:一条光线从表面反射,另一条光线经过表面折射。然后,这些光线的最终颜色将根据某些函数组合,并最终用作光线来自像素的颜色。我的问题是,我无法在着色器代码中拆分光线,因为我需要使用递归才能这样做。据我了解,着色器中的函数不能递归,因为由于与旧GPU硬件的兼容性问题,所有GLSL函数都类似于C ++中的内联函数。
在着色器代码中是否可以模拟或伪造递归,或者我能否完全不使用递归而实现反射和折射?我无法想象在没有递归的情况下如何发生,但我可能错了。

4
我猜你正在使用蒙特卡罗技术,然后随机选择反射或折射。 - Plow
我认为你可以尝试迭代。制作一个光线列表进行处理,只需将其添加到列表中...对于有限的递归层数,例如7条光线,您可以将其硬编码到静态数组中。另一种选择是使用几何着色器,在其中发射新的光线,但我不知道是否可行,因为我不知道您如何传递数据的架构。 - Spektre
我添加了**[edit1]**更新的代码。因此,概念验证有效。但是,使用32位浮点数的递归超过4级会变得缓慢,但仍比CPU光线追踪器快一个数量级。 - Spektre
GLSL不允许递归函数,但有时可以使用尾调用优化来消除递归。 - Anderson Green
1个回答

10
我成功地将背向追踪转换成了适用于GLSL的迭代过程,使用了我在评论中提出的方法。虽然离优化还有很大的距离,而且我还没有实现所有的物理特性(如斯涅尔定律等...),但作为概念证明已经可以工作了。我把所有的东西都放在片段着色器里,CPU端的代码只是以32位非夹紧浮点纹理GL_LUMINANCE32F_ARB的形式发送uniforms常量和场景。渲染只是单一的QUAD覆盖整个屏幕。
  1. 传递场景

我决定将场景存储在纹理中,这样每个射线/片段就可以直接访问整个场景。这个纹理是2D的,但它被用作32位浮点数的线性列表。我选择了这种格式:

    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,
        }; 

您可以添加/更改任何类型的对象。本示例仅包含单个半透明蓝色四面体。您还可以添加变换矩阵、材料属性的更多系数等...

  1. 架构

顶点着色器只初始化视图的角落光线(起始位置和方向),这些光线进行插值,因此每个片段表示后向光线追踪过程的起始光线。

迭代后向光线跟踪

因此,我创建了一个“静态”光线列表,并用起始光线进行初始化。迭代分为两个步骤,首先是后向光线跟踪:

  1. 循环遍历来自第一个的所有光线
  2. 找到与场景的最近交点...

将位置、表面法线和材料属性存储到光线 struct

  1. 如果找到交点且不是最后一层“递归”,则在列表末尾添加反射/折射光线。

同时,将它们的索引存储到已处理的光线struct中。

现在,您的光线应该包含您需要重建颜色所需的所有交点信息。为此:

  1. 倒序遍历所有递归级别
  2. 对于匹配当前递归层的每个光线
  3. 计算光线颜色

因此,使用您想要的灯光方程。如果光线包含子节点,请根据材料属性(反射和折射系数...)将其颜色添加到结果中。

现在,第一条光线应该包含您要输出的颜色。

使用的Uniforms:


tm_eye视图摄像机矩阵
aspect视图ys/xs纵横比
n0空间折射率(尚未使用)
focal_length相机焦距
fac_siz场景方形纹理的分辨率
fac_num实际使用的浮点数数量
fac_txr场景纹理单元

预览:

preview

片段着色器包含我的调试打印,因此如果使用纹理,则还需要该纹理,请参见QA:

待办事项:


为对象、相机等添加矩阵。
添加材质属性(光泽度,反射/折射系数)。
斯涅尔定律:现在新光线的方向是错误的……
可以将R,G,B分别作为三个起始光线,最后再合并。
基于光线长度伪造SSS次表面散射。
更好地实现灯光(现在它们只是代码中的常量)。
实现更多基元(现在仅支持三角形)。

[编辑1] 代码调试和升级

我删除了旧的源代码以适应30KB的限制。如果您需要它,请从编辑历史记录中查找。我有更多时间进行高级调试,以下是结果:

preview

此版本解决了一些几何、精度、域问题和错误。我已经实现了反射和折射,如测试光线所示:

debug view

在调试视图中,只有方块是透明的,最后一条未命中任何物体的光线被忽略。所以你可以看到光线分裂了……由于全反射角度,光线结束在方块内部。我出于速度原因禁用了物体内部的所有反射。

32位的floats用于交点检测时会有一些噪声,所以您可以改用64位的doubles,但速度会显著下降。另一个选择是重写方程以使用相对坐标,在这种情况下更精确。

这里是float着色器源代码:

顶点:

//------------------------------------------------------------------
#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);
    }
//------------------------------------------------------------------

碎片:

//------------------------------------------------------------------
#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;i0<rays;i0++)
        {
        // loop through all objects
        // find closest forward intersection between them and ray[i0]
        // strore it to ray[i0].(nor,col)
        // strore it to pos,n1
        t=tt=-1.0; ii=1; ray[i0].l=0.0;
        ray[i0].col=back_col;
        pos=ray[i0].pos; n1=n0;
        for (st=vec2(0.0,0.0),i=j=0;i<fac_num;)
            {
            c.r=fac_get;            // RGBA
            c.g=fac_get;
            c.b=fac_get;
            refl=fac_get;
            refr=fac_get;
            n1=fac_get;             // refraction index
            a=fac_get; id=int(a);   // object type
            a=fac_get; num=int(a);  // face count

            if (id==_fac_triangles)
             for (;num>0;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-u-v;
                    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;ii<rays;ii++)
            {
            txtsiz=0;
            txt_decimal(ray[ii].lvl);
            txt_print(dbg_x0,dbg_y0+ii+1);
            }

        for (ii=0,st=vec2(0.0,0.0),i=j=0;i<fac_num;ii++)
            {
            c.r=fac_get;            // RGBA
            txtsiz=0;
            txt_decimal(c.r);
            txt_print(dbg_x0,dbg_y0+ii+1);
            }
        if (_txt_col) col=txt_col.rgb;
        }
*/
    if (_dbg)
        {
        float x=dbg_x0,y=dbg_y0;
        vec3 a=vec3(1.0,2.0,3.0);
        vec3 b=vec3(5.0,6.0,7.0);
        txtsiz=0; txt_decimal(dot(a,b)); txt_print(x,y); y++;
        txtsiz=0; txt_decimal(cross(a,b)); txt_print(x,y); y++;
        if (_txt_col) col=txt_col.rgb;
        }
    #endif

    frag_col=vec4(col,1.0);
    }
//---------------------------------------------------------------------------
#ifdef _debug_print
//---------------------------------------------------------------------------
void txt_decimal(vec2 v)        // print vec2 into txt
    {
                      txt[txtsiz]='('; txtsiz++;
    txt_decimal(v.x); txt[txtsiz]=','; txtsiz++;
    txt_decimal(v.y); txt[txtsiz]=')'; txtsiz++;
    txt[txtsiz]=0;  // string terminator
    }
//---------------------------------------------------------------------------
void txt_decimal(vec3 v)        // print vec3 into txt
    {
                      txt[txtsiz]='('; txtsiz++;
    txt_decimal(v.x); txt[txtsiz]=','; txtsiz++;
    txt_decimal(v.y); txt[txtsiz]=','; txtsiz++;
    txt_decimal(v.z); txt[txtsiz]=')'; txtsiz++;
    txt[txtsiz]=0;  // string terminator
    }
//---------------------------------------------------------------------------
void txt_decimal(vec4 v)        // print vec4 into txt
    {
                      txt[txtsiz]='('; txtsiz++;
    txt_decimal(v.x); txt[txtsiz]=','; txtsiz++;
    txt_decimal(v.y); txt[txtsiz]=','; txtsiz++;
    txt_decimal(v.z); txt[txtsiz]=','; txtsiz++;
    txt_decimal(v.w); txt[txtsiz]=')'; txtsiz++;
    txt[txtsiz]=0;  // string terminator
    }
//---------------------------------------------------------------------------
void txt_decimal(float x)       // print float x into txt
    {
    int i,j,c;                  // l is size of string
    float y,a;
    const float base=10;
    // handle sign
    if (x<0.0) { txt[txtsiz]='-'; txtsiz++; x=-x; }
     else      { txt[txtsiz]='+'; txtsiz++; }
    // divide to int(x).fract(y) parts of number
    y=x; x=floor(x); y-=x;
    // handle integer part
    i=txtsiz;                   // start of integer part
    for (;txtsiz<_txtsiz;)
        {
        a=x;
        x=floor(x/base);
        a-=base*x;
        txt[txtsiz]=int(a)+'0'; txtsiz++;
        if (x<=0.0) break;
        }
    j=txtsiz-1;                 // end of integer part
    for (;i<j;i++,j--)          // reverse integer digits
        {
        c=txt[i]; txt[i]=txt[j]; txt[j]=c;
        }
    // handle fractional part
    for (txt[txtsiz]='.',txtsiz++;txtsiz<_txtsiz;)
        {
        y*=base;
        a=floor(y);
        y-=a;
        txt[txtsiz]=int(a)+'0'; txtsiz++;
        if (y<=0.0) break;
        }
    txt[txtsiz]=0;  // string terminator
    }
//---------------------------------------------------------------------------
void txt_decimal(int x)     // print int x into txt
    {
    int a,i,j,c;            // l is size of string
    const int base=10;
    // handle sign
    if (x<0.0) { txt[txtsiz]='-'; txtsiz++; x=-x; }
     else      { txt[txtsiz]='+'; txtsiz++; }
    // handle integer part
    i=txtsiz;               // start of integer part
    for (;txtsiz<_txtsiz;)
        {
        a=x;
        x/=base;
        a-=base*x;
        txt[txtsiz]=int(a)+'0'; txtsiz++;
        if (x<=0) break;
        }
    j=txtsiz-1;             // end of integer part
    for (;i<j;i++,j--)      // reverse integer digits
        {
        c=txt[i]; txt[i]=txt[j]; txt[j]=c;
        }
    txt[txtsiz]=0;  // string terminator
    }
//---------------------------------------------------------------------------
void txt_print(float x0,float y0)   // print txt at x0,y0 [chars]
    {
    int i;
    float x,y;
    // fragment position [chars] relative to x0,y0
    x=0.5*(1.0+txt_pos.x)/txt_fxs; x-=x0;
    y=0.5*(1.0-txt_pos.y)/txt_fys; y-=y0;
    // inside bbox?
    if ((x<0.0)||(x>float(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
//---------------------------------------------------------------------------

代码还没有被优化,我先想让物理正确运行。目前还没有实现Fresnells,但是使用了材料的refl, refr系数。

同时你可以忽略调试打印信息(它们被#define封装起来)。

我为几何纹理构建了一个小类,这样我就可以轻松设置场景对象。这就是预览场景的初始化方式:

ray.beg();
//                 r   g   b 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();

重要的是计算法线面朝向物体外部,因为这用于检测内部/外部物体交叉。

P.S.

如果您有兴趣,这是我的体积三维背向光线追踪器:

这是支持半球体对象的“网格”光线追踪器的新版本:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接