【简介与代码下载】
本节会在【OptiX】第5个示例 递归反射、抗锯齿的基础上加上折射。本文的代码如下:
链接:https://pan.baidu.com/s/1mAXoYBMbx_gWtCliUZ5o0Q
提取码:20ba
下载解压后,请使用VS2015打开工程,并把当前配置调整为Debug和x64。另需要在默认路径安装OptiX6.0与CUDA10.0,可以参考我的这篇博文:【Optix】Optix介绍与示例编译
运行效果如下:
【理论基础-折射】
折射非常简单,只要求出折射光线的方向就像反射一样递归计算就可以了。针对折射角有个定律叫做snell定律,它定义了光从一种材质射入到另一种材质入身角和折射角之间的关系:
其中n1和n2分别是两个介质的折射率,而折射率都是已知的。由此就可以求出折射方向了。在OptiX给我们提供了refract函数,它的原型如下:r是出参反射方向,i是入射光向,n是表面法线,ior则是折射率之比。
/**
* Calculates refraction direction
* r : refraction vector
* i : incident vector
* n : surface normal
* ior : index of refraction ( n2 / n1 )
* returns false in case of total internal reflection, in that case r is
* initialized to (0,0,0).
*/
OPTIXU_INLINE RT_HOSTDEVICE bool refract(float3& r, const float3& i, const float3& n, const float ior)
{
float3 nn = n;
float negNdotV = dot(i,nn);
float eta;
if (negNdotV > 0.0f)
{
eta = ior;
nn = -n;
negNdotV = -negNdotV;
}
else
{
eta = 1.f / ior;
}
const float k = 1.f - eta*eta * (1.f - negNdotV * negNdotV);
if (k < 0.0f) {
// Initialize this value, so that r always leaves this function initialized.
r = make_float3(0.f);
return false;
} else {
r = normalize(eta*i - (eta*negNdotV + sqrtf(k)) * nn);
return true;
}
}
【理论基础-折射与反射比】
当光照射向物体时,究竟多少折射多少反射,此时又有个定律叫做fresnel定律,schlick使用入射光与法线的夹角这一主要项,辅助fresnel_exponent等来估算出折射与反射比,在OptiX中有如下函数:返回值就是[0, 1]的值,代表反射率,反射率与折射率的和为1。
/** Schlick approximation of Fresnel reflectance
*/
OPTIXU_INLINE RT_HOSTDEVICE float fresnel_schlick(const float cos_theta, const float exponent = 5.0f,
const float minimum = 0.0f, const float maximum = 1.0f)
{
/**
Clamp the result of the arithmetic due to floating point precision:
the result should lie strictly within [minimum, maximum]
return clamp(minimum + (maximum - minimum) * powf(1.0f - cos_theta, exponent),
minimum, maximum);
*/
/** The max doesn't seem like it should be necessary, but without it you get
annoying broken pixels at the center of reflective spheres where cos_theta ~ 1.
*/
return clamp(minimum + (maximum - minimum) * powf(fmaxf(0.0f,1.0f - cos_theta), exponent),
minimum, maximum);
}
【射入物体的光线】
本例是计算了一个玻璃球,情况比没有折射的复杂了一点,也即光线的发生点可能在球体内部,当我们从球的外部观察球体时,发出的光线与球体相撞,相撞点叫做t_hit,而后以t_hit为起点,以折射光线与方向又在球内部发生光线与球相撞,以此类堆。光线在球外向球内传播时有折射率之比,从球内传向球外则折射率之比与球外向球内是互为倒数的。
我们来看代码:
float iSphereOutside(float3 ro, float3 rd)
{
float3 oc = ro - make_float3(sphere);
float b = dot(oc, rd);
float c = dot(oc, oc) - sphere.w * sphere.w;
float h = b*b - c;
if (h < 0.0)
return -1.0;
float t = (-b - sqrt(h));
return t;
}
float iSphereInside(float3 ro, float3 rd)
{
float3 oc = ro - make_float3(sphere);
float b1 = -dot(oc, rd);
float c2 = dot(oc, oc) - b1 * b1;
float b2 = sqrt(sphere.w*sphere.w - c2);
return b1+b2;
}
RT_PROGRAM void intersect_compute(int primIdx)
{
float3 O = ray.origin - make_float3(sphere);
float O_dot_O = dot(O, O);
float refraction_index = 1.0;
float t = -1;
float3 n;
if(O_dot_O > (sphere.w * sphere.w + scene_epsilon))
{
t = iSphereOutside(ray.origin, ray.direction);
n = normalize(ray.origin + t*ray.direction - make_float3(sphere));
//空气到玻璃
prd_radiance.refraction_index = 1.46;
}
else
{
//起点在球内则有且只有一个交点
t = iSphereInside(ray.origin, ray.direction);
n = normalize(make_float3(sphere)-(ray.origin + t*ray.direction) );
//玻璃到空气中
prd_radiance.refraction_index = 0.684;
}
if (t > 0)
{
if (rtPotentialIntersection(t))
{
float3 hit_p = ray.origin + t * ray.direction;
//在法线方向上求出一个微小的偏移,得出该点的球内外近点
geometric_normal = n;
float3 offset = normalize(geometric_normal) * scene_epsilon;
//球外点
front_hit_point = hit_p + offset;
//球内点
back_hit_point = hit_p - offset;
rtReportIntersection(0);
}
}
}
其中iSphereInside和iSphereOutside计算视点在球内和球外时与球的最近的交点。注意法线的方向也不相同,在球内的交点法线的方向是指向球心的,在上面代码中n的求解的不同反映到了这一点。沿法线的方向我们取一个极小的offset,用来计算撞击点球外和球内的点。球内的点用来做折射,球外的点可以用来做反射。有个偏移是避免再次与球相撞。
同样我们对球内和球外折射率也做了不同的设置。
最终我们在absGlass.cu中实现了玻璃的完全折射与反射的材质。以下图分另是代码做了拆分时不同的渲染效果。
上图左一是只有折射的效果。左二是:即有折射又有反射的效果。
第二行左一是对的折射赋了个颜色值,模拟有颜色的玻璃。左二是反射和折射都设置了颜色。