【简介与代码下载】
本文将通过一个示例来介绍光线生成模块,详细介绍光线生成,以及绘制一个三角形,对三角形在shader中实施着色从而介绍添加三角网在OptiX中的操作,并介绍当光线与三角形无交时,会调用相交丢失模块(Miss Program)的设置流程。光线的生成和场景的类似Trackball的操作器里包含着一个重要而又基础的知识点:与相机相关的空间变换。
本文示例代码:
链接:https://pan.baidu.com/s/19YUxZ3SFIDZposKIzpMHbQ
提取码:hhs3
下载解压后,请使用VS2015打开工程,并把当前配置调整为Debug和x64。另需要在默认路径安装OptiX6.0与CUDA10.0,可以参考我的这篇博文:【Optix】Optix介绍与示例编译
下面是运行结果,可以使用鼠标左键进行拖拽和右键进行拉近拉远。
【光线生成模块讲解】
本示例我们书写了一个三角形,其流程也较为简单,在光线生成模块程序camera.cu中,调用trTrace进行光线跟踪,在rtTrace中有对哪些物体进行跟踪,这个物体是场景树的顶层物体,示例中叫做top_object,rtTrace一旦调用,因此这个场景是我们传入的三角网,不是实时计算的物体,因此OptiX会自动帮我们计算交点,rtTrace调用时,会调用其关联的属性和材质point2color.cu,在rtTrace中相交,则实时调用point2color.cu中的triangle_attributes计算交点处的属性,而后若是离相机最近的交点则调用材质中的closest_hit_radiance来计算颜色放置在光线结构体prd_radiance的result成员中。在rtTrace结束后,其出参prd中的result就是材质中closest_hit_radiance设置的颜色。那么我们来首先来看第一步:生成光线。
生成光线按说是根据相机的位置,与要渲染的窗口上的每个像素相连,生成一条条光线与场景求交。在camera.cu中有一段很巧的代码:
//缓存尺寸,也就是屏幕尺寸
size_t2 screen = output_buffer.size();
//rtPrintf("%d-%d-", screen.x, screen.y);
float2 d = make_float2(launch_index) / make_float2(screen) * 2.f - 1.f;
float3 ray_origin = eye;
float3 ray_direction = normalize(d.x*U + d.y*V + W);
//生成光线
optix::Ray ray(ray_origin, ray_direction, RADIANCE_RAY_TYPE, scene_epsilon);
其中launch_index是当前像素的行列号。之前说过camera.cu做为光线发生程序,是要渲染的每个像素调用一次,每次的launch_index均不相同代表不同的像素,以生成不同的光线。我们来看d的值是什么,首先来看laun_index/screen,这个除了之后整个屏幕(本文中所说的屏幕都是渲染窗口)的范围是[0, 1],而又乘以2,范围是[0, 2],而又减去1则范围是[-1, 1],也即屏幕的[0, 0]到[width, height]被化成了d,也就是[-1, -1]到[1, 1]之间。每个像素均匀排列。
两个参数ray_origin代表光线的起点,就是眼睛的位置,由外部传入。而ray_direction的求解需要好好的解释解释。我们首先来看图:
其中e就是eye, l就是lookat,eye是光线的起点,方框代表要渲染的屏幕,分辨率是width/height,橙色的代表光线的方向,每个像素的中心点穿过一个光线。其中yv, zv, xv是相机的局部坐标轴,可注意是朝着z轴负方向的。而xw, yw, zw是世界坐标轴。现在要求每个光线的方向我们第一步是要求出相机的局部坐标系(xv, yv, zv)。再根据相机的fov和长宽比aspect_ratio计算出UVW,而后根据shader中d的值逐个遍历。和图中的d没有关系。
首先来看正交基xv, yv, zv的计算,其实很简单,因为eye已知,lookat已知,就如图图中的e和l,则两者相交就得到了一个向量,而后默认的up向量是(0, 1, 0)两者cross又得到了另一个向量,有了两个向量直接cross就得到了第三个向量,代码如下:
void calculateCameraVariables(optix::float3 eye, optix::float3 lookat, optix::float3 up,
float fov, float aspect_ratio,
optix::float3& U, optix::float3& V, optix::float3& W)
{
float ulen, vlen, wlen;
W = lookat - eye;
wlen = length(W);
U = normalize(cross(W, up));
V = normalize(cross(U, W));
vlen = wlen*tanf(0.5f * fov * M_PIf / 180.f);
V *= vlen;
ulen = vlen * aspect_ratio;
U *= ulen;
}
要注意看里面还有个tanf操作,求出的UVW是正交基,还要得到其视域的范围与之相乘才是放入到shader中要计算的UVW。参考fov与aspect_ratio的定义很容易就能够理解vlen和ulen的求法,如图:
其中红色的是wlen,而vlen是绿色的,蓝色的是ulen,而红色的与蓝色的夹角是fov的一半是已知量。aspect_ratio是整 个长宽的比率,也是已知量。由此可以求出UVW,传入到camera.cu中,从[-1, -1]到[1, 1]逐像素的遍历,就生成了光线。
【相机操作】
可见要实现对相机的操作就是求出UVW,要求出UVW就要计算eye, lookat, up这三个变量。假如平移操作,很好办,只要修改eye的位置就可以了,同步修改lookat的位置也行,其至lookat不改,eye不管走到哪里始终向它看也可以。复杂就复杂在鼠标左键的旋转上了。首先我们鼠标在屏幕上划动,计算出起始位置与结束位置,根据这两个位置要生成相机的旋转矩阵,而后与相机的矩阵相乘,最终实现变换。
而从起始位置到结整位置的计算我们使用到了两个技巧,一个是把屏幕坐标映射到球的大圆上,算是绕轴操作:
else if(mouse_button == GLUT_LEFT_BUTTON)
{
const optix::float2 from = { static_cast<float>(mouse_prev_pos.x), static_cast<float>(mouse_prev_pos.y) };
const optix::float2 to = { static_cast<float>(x), static_cast<float>(y) };
const optix::float2 a = { from.x / width, from.y / height };
const optix::float2 b = { to.x / width, to.y / height };
camera_rotate = arcball.rotate(b, a);
camera_dirty = true;
}
可以看到a,和b是归一化范围在[0, 1]的from和to,百后arcball实施了一个rotate操作得到了相机的旋转矩阵。那么rotate究竟做了什么。
optix::Matrix4x4 Arcball::rotate(const optix::float2& from, const optix::float2& to) const
{
optix::float3 a = toSphere(from);
optix::float3 b = toSphere(to);
Quaternion q = Quaternion(a, b);
q.normalize();
return q.rotationMatrix();
}
首先模拟球的大圆,得到了两个点a, b,相当于a, b点共球,然后从a点移到b点求旋转矩阵。这也就是这种操作器叫做trackball的原因。是把屏幕上的二维点模拟了一个三维球上的大圆的点,通过另外一维永远为0来实现的。
那么最后一个问题,四元数Quaternion 是如何实现根据起始点和结束点来计算旋转矩阵的。
【四元数】
参考这篇文章:彻底搞懂四元数
在计算完成旋转矩阵后,在updateCamera中更新UVW:
void updateCamera()
{
const float vFov = 30.0f;
const float aspect_radio = static_cast<float>(width) / static_cast<float>(height);
optix::float3 camera_u, camera_v, camera_w;
calculateCameraVariables(camera_eye, camera_lookat, camera_up, vFov, aspect_radio,
camera_u, camera_v, camera_w);
//相机坐标系,UVW是正交基
const optix::Matrix4x4 frame = optix::Matrix4x4::fromBasis(
normalize(camera_u),
normalize(camera_v),
normalize(-camera_w),
optix::make_float3(0)
);
//相机坐标系的逆变换
const optix::Matrix4x4 frame_inv = frame.inverse();
//frame_inv代表将点变换到相机空间,再乘以camera_rotate代表相机的旋转,再乘以frame代表再变到世界空间
const optix::Matrix4x4 trans = frame*camera_rotate*frame_inv;
//将camera_rotate施加的变换后重新计算eye, lookat, up,再重新计算相机的正交基
camera_eye = optix::make_float3(trans * make_float4(camera_eye, 1.0f));
camera_lookat = optix::make_float3(trans * make_float4(camera_lookat, 1.0f));
camera_up = optix::make_float3(trans * make_float4(camera_up, 0.0f));
calculateCameraVariables(camera_eye, camera_lookat, camera_up, vFov, aspect_radio,
camera_u, camera_v, camera_w);
camera_rotate = optix::Matrix4x4::identity();
context["eye"]->setFloat(camera_eye);
context["U"]->setFloat(camera_u);
context["V"]->setFloat(camera_v);
context["W"]->setFloat(camera_w);
camera_dirty = false;
}
中间最关键的部分就是施加camera_rotate变换。关于此还可以参考这篇文章:详解MVP矩阵之ViewMatrix
【场景构建-输入三角形】
这属于API级的知识,在定义完顶点和顶点索引之后,直接通过API设置即可:
optix::GeometryTriangles geom_tri = context->createGeometryTriangles();
geom_tri->setPrimitiveCount(num_faces);
geom_tri->setTriangleIndices(index_buffer, RT_FORMAT_UNSIGNED_INT3);
geom_tri->setVertices(num_vertices, vertex_buffer, RT_FORMAT_FLOAT3);
geom_tri->setBuildFlags(RTgeometrybuildflags(0));
其中setPrimitiveCount设置的是三角形的数量。注意OptiX是针对元数据也就是每个primitive来重新构建加速结构的。
【计算属性与材质】
当场景中输入三角形后,OptiX会自动的计算交点,当有交点时,会需要实时的计算交点的属性,比如法线、纹理坐标等,然后再调用材质中的相关内容根据这些属性计算出最终要显示的颜色。
上面解释的是属性的调用时机与纹理的调用时机。通过如下代码设置属性shader与材质shader以及它们的启动函数。
//创建三角形的属性生成模块
const char* ptx = getPtxString("point2color.cu");
geom_tri->setAttributeProgram(context->createProgramFromPTXString(ptx, "triangle_attributes"));
geom_tri["index_buffer"]->setBuffer(index_buffer);
geom_tri["vertex_buffer"]->setBuffer(vertex_buffer);
//创建材质
optix::Program bary_ch = context->createProgramFromPTXString(ptx, "closest_hit_radiance");
optix::Material bary_matl = context->createMaterial();
bary_matl->setClosestHitProgram(0, bary_ch);
【光线相交丢失模块】
当光线与场景不相交时,什么材质属性都不好使了,那么屏幕要显示什么颜色呢,OptiX通过miss program来实现:
#include <optix_world.h>
using namespace optix;
rtDeclareVariable(float3, bg_color, , );
struct PerRayData_radiance
{
float3 result;
};
rtDeclareVariable(PerRayData_radiance, prd_radiance, rtPayload, );
RT_PROGRAM void miss()
{
prd_radiance.result = bg_color;
}
在CPU端通过如下代码设置shader和回调函数:
context->setMissProgram(0, context->createProgramFromPTXString(getPtxString("miss.cu"), "miss"));
context["bg_color"]->setFloat(1.0f, 0.0f, 1.0f);