Optix光线追踪渲染器(三)复杂模型纹理、简单阴影与时域降噪
- 27 3 月, 2023
- by
- pladmin
引言
在上一篇文章中,我们完成了多模型多材质场景的渲染,此时我们已经可以自由地为场景添加很多模型了。但是一个场景如果只有简单的方块形状,而且没有图案,那渲染出来的效果可谓不堪入目,实在对不起optix底层这么大的算力。所以这一节,我们逐步引入大型模型、纹理映射、追加阴影透射等内容。
Part1. 复杂模型导入
导入外部模型需要借助第三方库tiny_obj_loader,调用其LoadObj读取.obj和.mtl文件便可得到所有顶点、索引、法线和材质了。但是,事实上并没有这么简单,因为我们要考虑一件事:一个TriangleMesh对应一个shader Record(即材质)。如果导入的obj是一个模型,但是它身上有多个材质,那么我们将不得不对这个模型进行拆分成多个TriangleMesh,从而映射多套材质。
这是一个相当麻烦的过程,所以我们新建了个Model.h/Model.cpp文件来专门处理模型导入和拆分的事。
Model.h:
#pragma once
#include "gdt/math/AffineSpace.h"
#include <vector>
/*! \namespace osc - Optix Siggraph Course */
namespace osc {
using namespace gdt;
/*! a simple indexed triangle mesh that our sample renderer will
render */
struct TriangleMesh {
std::vector<vec3f> vertex;
std::vector<vec3f> normal;
std::vector<vec2f> texcoord;
std::vector<vec3i> index;
// material data:
vec3f diffuse;
};
struct Model {
~Model()
{
for (auto mesh : meshes) delete mesh;
}
std::vector<TriangleMesh*> meshes;
//! bounding box of all vertices in the model
box3f bounds;
};
Model* loadOBJ(const std::string& objFile);
}
首先我们将TriangleMesh的定义移动到Model.h中,并重新定义它的成员参数:vertex(顶点)、normal(法线)、texcoord(uv)、index(三角面索引),这里要注意vertex[k]和normal[k]和texcoord[k]是一一对应的,共同表示模型中索引号为k的点的属性,而index就是存每个三角形中使用顶点的索引号。这里又预存了一个diffuse(漫反射颜色,当然我个人不是很喜欢把材质的内容直接定义在模型结构体内部)
后面定义了一个Model结构体,Model指的是导入的单个模型文件,用meshs管理所有拆分后的模型;bounds是一个包围盒,理论上包围盒需要根据模型的所有顶点计算出模型所覆盖的空间,这样就可以计算出模型的中心点在哪里了。
接下来我们要用loadOBJ来读取.obj文件了,在读取之前,我们还要对.obj文件有一点额外的了解:首先,一个obj文件中可能会有多个shape;顶点、法线、uv、三角面都是分shape存储的。什么是shape呢?比如我们在Blender中创建了一个立方体物体,一个圆柱物体,导出的时候我们把这两个物体导出到同一个.obj中了,那么此时.obj物体就拥有两个shape;而对于每个shape,可以拥有多个材质通道。所以这个.obj中存在多少个材质通道,就需要把.obj的模型切分成多少个TriangleMesh。
我们慢慢来看Model.cpp中的内容:
#include "Model.h"
#define TINYOBJLOADER_IMPLEMENTATION
#include "3rdParty/tiny_obj_loader.h"
//std
#include <set>
namespace std {
inline bool operator<(const tinyobj::index_t &a,
const tinyobj::index_t &b)
{
if (a.vertex_index < b.vertex_index) return true;
if (a.vertex_index > b.vertex_index) return false;
if (a.normal_index < b.normal_index) return true;
if (a.normal_index > b.normal_index) return false;
if (a.texcoord_index < b.texcoord_index) return true;
if (a.texcoord_index > b.texcoord_index) return false;
return false;
}
}
首先导入模型库,后面这一段是用来给index定义大小关系的,然而没有看到哪里用到<这个operator了。。。我们直接无视它。
Model *loadOBJ(const std::string &objFile)
{
Model *model = new Model;
const std::string mtlDir
= objFile.substr(0,objFile.rfind('/')+1);
PRINT(mtlDir);
tinyobj::attrib_t attributes;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string err = "";
bool readOK
= tinyobj::LoadObj(&attributes,
&shapes,
&materials,
&err,
&err,
objFile.c_str(),
mtlDir.c_str(),
/* triangulate */true);
if (!readOK) {
throw std::runtime_error("Could not read OBJ model from "+objFile+":"+mtlDir+" : "+err);
}
if (materials.empty())
throw std::runtime_error("could not parse materials ...");
std::cout << "Done loading obj file - found " << shapes.size() << " shapes with " << materials.size() << " materials" << std::endl;
for (int shapeID=0;shapeID<(int)shapes.size();shapeID++) {
tinyobj::shape_t &shape = shapes[shapeID];
std::set<int> materialIDs;
for (auto faceMatID : shape.mesh.material_ids)
materialIDs.insert(faceMatID);
std::map<tinyobj::index_t,int> knownVertices;
for (int materialID : materialIDs) {
TriangleMesh *mesh = new TriangleMesh;
for (int faceID=0;faceID<shape.mesh.material_ids.size();faceID++) {
if (shape.mesh.material_ids[faceID] != materialID) continue;
tinyobj::index_t idx0 = shape.mesh.indices[3*faceID+0];
tinyobj::index_t idx1 = shape.mesh.indices[3*faceID+1];
tinyobj::index_t idx2 = shape.mesh.indices[3*faceID+2];
vec3i idx(addVertex(mesh, attributes, idx0, knownVertices),
addVertex(mesh, attributes, idx1, knownVertices),
addVertex(mesh, attributes, idx2, knownVertices));
mesh->index.push_back(idx);
mesh->diffuse = (const vec3f&)materials[materialID].diffuse;
mesh->diffuse = gdt::randomColor(materialID);
}
if (mesh->vertex.empty())
delete mesh;
else
model->meshes.push_back(mesh);
}
}
// of course, you should be using tbb::parallel_for for stuff
// like this:
for (auto mesh : model->meshes)
for (auto vtx : mesh->vertex)
model->bounds.extend(vtx);
std::cout << "created a total of " << model->meshes.size() << " meshes" << std::endl;
return model;
}
loadOBJ函数就是读取、处理模型的函数。首先我们调用tinyObj的LoadObj接口来打开模型,此时会将模型的所有顶点、法线、uv、索引信息存入attributes;将obj中的各个shape存入shapes;将所有材质信息存入materials。
接下来,我们需要按照材质来拆分模型(一个材质对应一个TriangleMesh)。首先一个obj文件有若干个shapes;一个shapes又有若干个材质。所以需要遍历所有shapes;在shapes中,再遍历该shapes内的所有material,对于每一个material,我们创建一个新的TriangleMesh,这里就要开始拆分了,我们遍历该shapes下每一个 材质是当前所遍历材质 的面,我们可以直接得到这个面的索引信息(包括三个顶点的编号、三个顶点的uv号、三个顶点的法线号),然后把它们“塞入”这个新TriangleMesh里。
逻辑其实比较明确,只是怎么“塞入”稍显复杂。这里我们创建了一个addVertex函数,来完成塞入:
int addVertex(TriangleMesh *mesh,
tinyobj::attrib_t &attributes,
const tinyobj::index_t &idx,
std::map<tinyobj::index_t,int> &knownVertices)
{
if (knownVertices.find(idx) != knownVertices.end())
return knownVertices[idx];
const vec3f *vertex_array = (const vec3f*)attributes.vertices.data();
const vec3f *normal_array = (const vec3f*)attributes.normals.data();
const vec2f *texcoord_array = (const vec2f*)attributes.texcoords.data();
int newID = mesh->vertex.size();
knownVertices[idx] = newID;
mesh->vertex.push_back(vertex_array[idx.vertex_index]);
if (idx.normal_index >= 0) {
while (mesh->normal.size() < mesh->vertex.size())
mesh->normal.push_back(normal_array[idx.normal_index]);
}
if (idx.texcoord_index >= 0) {
while (mesh->texcoord.size() < mesh->vertex.size())
mesh->texcoord.push_back(texcoord_array[idx.texcoord_index]);
}
// just for sanity's sake:
if (mesh->texcoord.size() > 0)
mesh->texcoord.resize(mesh->vertex.size());
// just for sanity's sake:
if (mesh->normal.size() > 0)
mesh->normal.resize(mesh->vertex.size());
return newID;
}
addVertex函数需要几个传参:首先需要我们新建的TriangleMesh,然后是obj模型的全部信息attributes,然后是当前遍历到的面的三个索引信息(包括选用几号顶点、几号uv、几号法线),以及一个检查是否有重复索引的map结构knownVertices。
刚进函数时,首先要判断当前这个idx索引信息是否已经有了(即已经出现在了knownVertices里),如果有了,那我们就不需要创建新顶点,直接返回模型的索引编号即可;如果发现这个索引信息还没有,那我们就向TriangleMesh中放入这个新的顶点信息(包括顶点坐标、uv值、法线方向,这些都在attributes里能找到),然后把他记录在knownVertices中;如果之后有三角形的某个点又用到了相同的索引,就直接返回这个索引编号即可。
addVertex会返回新顶点信息的索引号,这样连续三个索引号就可以构成一个TriangleMesh的新三角面了。最后,我们遍历所有顶点来计算bounds的空间范围,方便后面找模型的中心位置。
现在我们将场景中的meshes组改成了Model类,SampleRenderer的成员参数和构造都要跟着改,这里都是细枝末节了所以就不多赘述代码怎么改了。最后修改一下main.cpp实现模型导入:
main.cpp:
extern "C" int main(int ac, char** av)
{
try {
Model* model = loadOBJ("../../models/sponza.obj");
Camera camera = { /*from*/vec3f(-1293.07f, 154.681f, -0.7304f),
/* at */model->bounds.center() - vec3f(0,400,0),
/* up */vec3f(0.f,1.f,0.f) };
// something approximating the scale of the world, so the
// camera knows how much to move for any given user interaction:
const float worldScale = length(model->bounds.span());
SampleWindow* window = new SampleWindow("Optix 7 Course Example",
model, camera, worldScale);
window->run();
}
catch (std::runtime_error& e) {
std::cout << GDT_TERMINAL_RED << "FATAL ERROR: " << e.what()
<< GDT_TERMINAL_DEFAULT << std::endl;
std::cout << "Did you forget to copy sponza.obj and sponza.mtl into your optix7course/models directory?" << std::endl;
exit(1);
}
return 0;
}
最后在McGuire Computer Graphics Archive (casual-effects.com)网站下,下载一个场景模型放到对应目录,渲染:

Part.2 纹理导入
纹理是ClosetHit Shader用来计算某一点的漫射颜色的,这意味着我们需要在HitGroup Record的data中绑定纹理。由于data是我们自定义的TriangleMeshSBTData结构,因此修改TriangleMeshSBTData:
struct TriangleMeshSBTData {
vec3f color;
vec3f *vertex;
vec3f *normal;
vec2f *texcoord;
vec3i *index;
bool hasTexture;
cudaTextureObject_t texture;
};
这里我们把法线、uv和纹理全部传入,这样在ClosetHit Shader中可以获取的信息就更丰富了。
接下来,我们向Model和TriangleMesh中也引入纹理的概念:
Model.h:
struct TriangleMesh {
std::vector<vec3f> vertex;
std::vector<vec3f> normal;
std::vector<vec2f> texcoord;
std::vector<vec3i> index;
// material data:
vec3f diffuse;
int diffuseTextureID { -1 };
};
struct Texture {
~Texture()
{ if (pixel) delete[] pixel; }
uint32_t *pixel { nullptr };
vec2i resolution { -1 };
};
struct Model {
~Model()
{
for (auto mesh : meshes) delete mesh;
for (auto texture : textures) delete texture;
}
std::vector<TriangleMesh *> meshes;
std::vector<Texture *> textures;
//! bounding box of all vertices in the model
box3f bounds;
};
TriangleMesh中加入了一项diffuseTextureID表示当前模型用的是几号贴图,这样在绑定SBT的时候方便向HitGroup Record->data中的texture赋值。同时定义了一个Texture架构体,设置了一个分辨率参数,以及贴图像素指针。对于Model结构体,增加一个textures数组,用来装载该obj所涉及的全部贴图。
接下来编写加载贴图的函数:
int loadTexture(Model *model,
std::map<std::string,int> &knownTextures,
const std::string &inFileName,
const std::string &modelPath)
{
if (inFileName == "")
return -1;
if (knownTextures.find(inFileName) != knownTextures.end())
return knownTextures[inFileName];
std::string fileName = inFileName;
// first, fix backspaces:
for (auto &c : fileName)
if (c == '\\') c = '/';
fileName = modelPath+"/"+fileName;
vec2i res;
int comp;
unsigned char* image = stbi_load(fileName.c_str(),
&res.x, &res.y, &comp, STBI_rgb_alpha);
int textureID = -1;
if (image) {
textureID = (int)model->textures.size();
Texture *texture = new Texture;
texture->resolution = res;
texture->pixel = (uint32_t*)image;
/* iw - actually, it seems that stbi loads the pictures
mirrored along the y axis - mirror them here */
for (int y=0;y<res.y/2;y++) {
uint32_t *line_y = texture->pixel + y * res.x;
uint32_t *mirrored_y = texture->pixel + (res.y-1-y) * res.x;
int mirror_y = res.y-1-y;
for (int x=0;x<res.x;x++) {
std::swap(line_y[x],mirrored_y[x]);
}
}
model->textures.push_back(texture);
} else {
std::cout << GDT_TERMINAL_RED
<< "Could not load texture from " << fileName << "!"
<< GDT_TERMINAL_DEFAULT << std::endl;
}
knownTextures[inFileName] = textureID;
return textureID;
}
这段函数就是根据纹理的名称,来打开纹理文件、给其赋予贴图编号、将其绑定在Model->textures上。knownTextures是为了防止重复绑定相同纹理的map记录。
Model* loadOBJ(const std::string& objFile)
{
Model* model = new Model;
const std::string modelDir
= objFile.substr(0, objFile.rfind('/') + 1);
...
std::map<std::string, int> knownTextures;
...
for (int faceID=0;faceID<shape.mesh.material_ids.size();faceID++) {
if (shape.mesh.material_ids[faceID] != materialID) continue;
tinyobj::index_t idx0 = shape.mesh.indices[3*faceID+0];
tinyobj::index_t idx1 = shape.mesh.indices[3*faceID+1];
tinyobj::index_t idx2 = shape.mesh.indices[3*faceID+2];
vec3i idx(addVertex(mesh, attributes, idx0, knownVertices),
addVertex(mesh, attributes, idx1, knownVertices),
addVertex(mesh, attributes, idx2, knownVertices));
mesh->index.push_back(idx);
mesh->diffuse = (const vec3f&)materials[materialID].diffuse;
mesh->diffuseTextureID = loadTexture(model,
knownTextures,
materials[materialID].diffuse_texname,
modelDir);
}
回到LoadObj函数,我们就需要提前创建一个knownTextures;同时当我们在创建新的TriangleMesh的时候,调用LoadTextures来加载当前材质所需的贴图并获得该贴图编号,并将编号赋值给TriangleMesh的diffuseTextureID上。
因为这一example我们需要引入法线、uv,而且涉及贴图绑定的内容,因此需要在SampleRenderer.h中额外申请以下变量:
std::vector<CUDABuffer> normalBuffer;
std::vector<CUDABuffer> texcoordBuffer;
std::vector<cudaArray_t> textureArrays;
std::vector<cudaTextureObject_t> textureObjects;
在渲染器进行构建的时候,我们需要在管线创建完成之后、建立SBT之前,将纹理全部注册:
void SampleRenderer::createTextures()
{
int numTextures = (int)model->textures.size();
textureArrays.resize(numTextures);
textureObjects.resize(numTextures);
for (int textureID=0;textureID<numTextures;textureID++) {
auto texture = model->textures[textureID];
cudaResourceDesc res_desc = {};
cudaChannelFormatDesc channel_desc;
int32_t width = texture->resolution.x;
int32_t height = texture->resolution.y;
int32_t numComponents = 4;
int32_t pitch = width*numComponents*sizeof(uint8_t);
channel_desc = cudaCreateChannelDesc<uchar4>();
cudaArray_t &pixelArray = textureArrays[textureID];
CUDA_CHECK(MallocArray(&pixelArray,
&channel_desc,
width,height));
CUDA_CHECK(Memcpy2DToArray(pixelArray,
/* offset */0,0,
texture->pixel,
pitch,pitch,height,
cudaMemcpyHostToDevice));
res_desc.resType = cudaResourceTypeArray;
res_desc.res.array.array = pixelArray;
cudaTextureDesc tex_desc = {};
tex_desc.addressMode[0] = cudaAddressModeWrap;
tex_desc.addressMode[1] = cudaAddressModeWrap;
tex_desc.filterMode = cudaFilterModeLinear;
tex_desc.readMode = cudaReadModeNormalizedFloat;
tex_desc.normalizedCoords = 1;
tex_desc.maxAnisotropy = 1;
tex_desc.maxMipmapLevelClamp = 99;
tex_desc.minMipmapLevelClamp = 0;
tex_desc.mipmapFilterMode = cudaFilterModePoint;
tex_desc.borderColor[0] = 1.0f;
tex_desc.sRGB = 0;
// Create texture object
cudaTextureObject_t cuda_tex = 0;
CUDA_CHECK(CreateTextureObject(&cuda_tex, &res_desc, &tex_desc, nullptr));
textureObjects[textureID] = cuda_tex;
}
}
由于cpu和gpu储存、绑定纹理的形式是完全不同的,因此需要在gpu端对所有用到的纹理进行依次注册。上述代码主要是对所有纹理进行空间计算,并对纹理进行模式定义(滤波、格式等),然后将其绑定到GPU的通道中,最后的textureObjects就是可以被gpu所访问的纹理空间,这里装的纹理可以在Record中被绑定。要问具体每条api代表啥意思,我只能说太复杂了看不懂,略过。
接下来绑定一下SBT:
int numObjects = (int)model->meshes.size();
std::vector<HitgroupRecord> hitgroupRecords;
for (int meshID=0;meshID<numObjects;meshID++) {
auto mesh = model->meshes[meshID];
HitgroupRecord rec;
// all meshes use the same code, so all same hit group
OPTIX_CHECK(optixSbtRecordPackHeader(hitgroupPGs[0],&rec));
rec.data.color = mesh->diffuse;
if (mesh->diffuseTextureID >= 0) {
rec.data.hasTexture = true;
rec.data.texture = textureObjects[mesh->diffuseTextureID];
} else {
rec.data.hasTexture = false;
}
rec.data.index = (vec3i*)indexBuffer[meshID].d_pointer();
rec.data.vertex = (vec3f*)vertexBuffer[meshID].d_pointer();
rec.data.normal = (vec3f*)normalBuffer[meshID].d_pointer();
rec.data.texcoord = (vec2f*)texcoordBuffer[meshID].d_pointer();
hitgroupRecords.push_back(rec);
}
hitgroupRecordsBuffer.alloc_and_upload(hitgroupRecords);
sbt.hitgroupRecordBase = hitgroupRecordsBuffer.d_pointer();
sbt.hitgroupRecordStrideInBytes = sizeof(HitgroupRecord);
sbt.hitgroupRecordCount = (int)hitgroupRecords.size();
SBT部分就是多了一个纹理检测,对每个TriangleMesh检查diffuseTextureID,如果是-1代表没有纹理,就向Record中的hasTexture写入fase;如果≥0代表有纹理,将textureObjects中寄存的纹理赋值给Record中的texture。剩下就是要追加绑定一下normal和texcoord项,因为在shader中采样纹理需要用到各个顶点的texcoord。
最后一步,就是编写以下着色器代码。老样子,我们只需要改写ClosetHit Shader:
extern "C" __global__ void __closesthit__radiance()
{
const TriangleMeshSBTData &sbtData
= *(const TriangleMeshSBTData*)optixGetSbtDataPointer();
// ------------------------------------------------------------------
// gather some basic hit information
// ------------------------------------------------------------------
const int primID = optixGetPrimitiveIndex();
const vec3i index = sbtData.index[primID];
const float u = optixGetTriangleBarycentrics().x;
const float v = optixGetTriangleBarycentrics().y;
// ------------------------------------------------------------------
// compute normal, using either shading normal (if avail), or
// geometry normal (fallback)
// ------------------------------------------------------------------
vec3f N;
if (sbtData.normal) {
N = (1.f-u-v) * sbtData.normal[index.x]
+ u * sbtData.normal[index.y]
+ v * sbtData.normal[index.z];
} else {
const vec3f &A = sbtData.vertex[index.x];
const vec3f &B = sbtData.vertex[index.y];
const vec3f &C = sbtData.vertex[index.z];
N = normalize(cross(B-A,C-A));
}
N = normalize(N);
// ------------------------------------------------------------------
// compute diffuse material color, including diffuse texture, if
// available
// ------------------------------------------------------------------
vec3f diffuseColor = sbtData.color;
if (sbtData.hasTexture && sbtData.texcoord) {
const vec2f tc
= (1.f-u-v) * sbtData.texcoord[index.x]
+ u * sbtData.texcoord[index.y]
+ v * sbtData.texcoord[index.z];
vec4f fromTexture = tex2D<float4>(sbtData.texture,tc.x,tc.y);
diffuseColor *= (vec3f)fromTexture;
}
// ------------------------------------------------------------------
// perform some simple "NdotD" shading
// ------------------------------------------------------------------
const vec3f rayDir = optixGetWorldRayDirection();
const float cosDN = 0.2f + .8f*fabsf(dot(rayDir,N));
vec3f &prd = *(vec3f*)getPRD<vec3f>();
prd = cosDN * diffuseColor;
}
我们来详细解析一下这段Shader。首先调用了optixGetTriangleBarycentrics()函数,得到了交点在当前三角形的重心坐标。什么是重心坐标呢?假如有一个三角形ABC,取三角形内部一点P,满足AP=u*AB+v*AC,则(u,v)就是P点在该三角形内的1重心坐标。当P点位于B点时,u=1,v=0;当P点位于C点时,u=0,v=1;当P点位于A点时,u=0,v=0。因此我们可以用u描述P离B有多近,用v描述P离C有多近,用1-u-v描述P离A有多近。
之所以要用到重心坐标,是因为我们希望进行Phong式着色,即平滑法线。因为三角形的三个顶点的法线往往指向不同的方向,因此三角形面内的点就需要对三者的法线进行插值。事实上,交点离哪个顶点更近,那么插值哪个顶点的权重就更高,而重心坐标刚好就可以描述这种权重。当然,如果三角形没有法线数据,那么老老实实做叉乘求法线就好。
对于纹理采样,我们需要先求出交点的texcoord,其实求交点texcoord的算法和法线一样,也是以重心坐标为权重 来计算texcoord的插值。得到texcoord的u、v值后,去Record->data中绑定的纹理进行采样,便得到了该交点的纹理颜色。
编译cuda—parse ptx—编译工程—运行项目,喜提runtime error:

发现是有个CudaBuffer没有resize= =,最后解决了。

Part3. 阴影射线
截止到目前,我们的光追都不是真正的光追,因为所有射线从视角出发,弹射一次就结束了,直接把第一次弹射到的颜色作为最终结果输出,这样的算法自然导致场景中不存在阴影。正规方法肯定是要将后续的弹射全部补充上,不过这一节先不着急,我们再体验一下optix给我们内置的一种快速生成阴影的方法——阴影射线。
阴影射线是一种另类射线,我们假定某处有一个天光,当我们正常光追求交到某个顶点后,让这个顶点向假定的天光发射一根射线,如果未击中任何物体,说明能够抵达天光,那么就着亮色;如果击中了物体,说明天光被这个物体挡住了,那么就着暗色。(事实上,如果大家对重要性采样有了解,就会知道 是否能够直接对光源重要性采样,其实就是靠发射一次阴影射线来检验的。当然这一讲和重要性采样无关)
首先我们修改LaunchParams.h:
enum { RADIANCE_RAY_TYPE=0, SHADOW_RAY_TYPE, RAY_TYPE_COUNT };
struct {
vec3f origin, du, dv, power;
} light;
首先我们增加一个枚举项SHADOW_RAY_TYPE;同时增加一个假定的天光,包括它的位置、横纵范围和光强。
然后我们在model.h中增加一个光源的类:
struct QuadLight {
vec3f origin, du, dv, power;
};
相应的,我们要修改sampleRenderer的构造,把光源引入场景:
SampleRenderer::SampleRenderer(const Model *model, const QuadLight &light)
: model(model)
{
initOptix();
launchParams.light.origin = light.origin;
launchParams.light.du = light.du;
launchParams.light.dv = light.dv;
launchParams.light.power = light.power;
由于shadow ray是意义截然不同的射线,因此我们需要为它定义一个不同的miss shader和hit shader。那么在创建管线之前,我们就要预先创建出新的miss shader和hit shader实例:
void SampleRenderer::createMissPrograms()
{
// we do a single ray gen program in this example:
missPGs.resize(RAY_TYPE_COUNT);
char log[2048];
size_t sizeof_log = sizeof( log );
OptixProgramGroupOptions pgOptions = {};
OptixProgramGroupDesc pgDesc = {};
pgDesc.kind = OPTIX_PROGRAM_GROUP_KIND_MISS;
pgDesc.miss.module = module ;
// ------------------------------------------------------------------
// radiance rays
// ------------------------------------------------------------------
pgDesc.miss.entryFunctionName = "__miss__radiance";
OPTIX_CHECK(optixProgramGroupCreate(optixContext,
&pgDesc,
1,
&pgOptions,
log,&sizeof_log,
&missPGs[RADIANCE_RAY_TYPE]
));
if (sizeof_log > 1) PRINT(log);
// ------------------------------------------------------------------
// shadow rays
// ------------------------------------------------------------------
pgDesc.miss.entryFunctionName = "__miss__shadow";
OPTIX_CHECK(optixProgramGroupCreate(optixContext,
&pgDesc,
1,
&pgOptions,
log,&sizeof_log,
&missPGs[SHADOW_RAY_TYPE]
));
if (sizeof_log > 1) PRINT(log);
}
/*! does all setup for the hitgroup program(s) we are going to use */
void SampleRenderer::createHitgroupPrograms()
{
// for this simple example, we set up a single hit group
hitgroupPGs.resize(RAY_TYPE_COUNT);
char log[2048];
size_t sizeof_log = sizeof( log );
OptixProgramGroupOptions pgOptions = {};
OptixProgramGroupDesc pgDesc = {};
pgDesc.kind = OPTIX_PROGRAM_GROUP_KIND_HITGROUP;
pgDesc.hitgroup.moduleCH = module;
pgDesc.hitgroup.moduleAH = module;
// -------------------------------------------------------
// radiance rays
// -------------------------------------------------------
pgDesc.hitgroup.entryFunctionNameCH = "__closesthit__radiance";
pgDesc.hitgroup.entryFunctionNameAH = "__anyhit__radiance";
OPTIX_CHECK(optixProgramGroupCreate(optixContext,
&pgDesc,
1,
&pgOptions,
log,&sizeof_log,
&hitgroupPGs[RADIANCE_RAY_TYPE]
));
if (sizeof_log > 1) PRINT(log);
// -------------------------------------------------------
// shadow rays: technically we don't need this hit group,
// since we just use the miss shader to check if we were not
// in shadow
// -------------------------------------------------------
pgDesc.hitgroup.entryFunctionNameCH = "__closesthit__shadow";
pgDesc.hitgroup.entryFunctionNameAH = "__anyhit__shadow";
OPTIX_CHECK(optixProgramGroupCreate(optixContext,
&pgDesc,
1,
&pgOptions,
log,&sizeof_log,
&hitgroupPGs[SHADOW_RAY_TYPE]
));
if (sizeof_log > 1) PRINT(log);
}
...
for (int meshID = 0; meshID < numObjects; meshID++) {
for (int rayID = 0; rayID < RAY_TYPE_COUNT; rayID++) {
auto mesh = model->meshes[meshID];
HitgroupRecord rec;
OPTIX_CHECK(optixSbtRecordPackHeader(hitgroupPGs[rayID], &rec));
rec.data.color = mesh->diffuse;
if (mesh->diffuseTextureID >= 0) {
rec.data.hasTexture = true;
rec.data.texture = textureObjects[mesh->diffuseTextureID];
}
else {
rec.data.hasTexture = false;
}
rec.data.index = (vec3i*)indexBuffer[meshID].d_pointer();
rec.data.vertex = (vec3f*)vertexBuffer[meshID].d_pointer();
rec.data.normal = (vec3f*)normalBuffer[meshID].d_pointer();
rec.data.texcoord = (vec2f*)texcoordBuffer[meshID].d_pointer();
hitgroupRecords.push_back(rec);
}
}
其实改动不大,就是把missPG、anyHitPG、closestHitPG都扩充到了两个,然后分别注册、绑定到对应的入口。注意:shadow ray对应的入口函数名分别是__miss__shadow、__closesthit__shadow和__anyhit__shadow,到时候在.cu文件中要定义这三个函数。
现在我们来看一下着色器的具体写法:
extern "C" __global__ void __closesthit__shadow()
{
/* not going to be used ... */
}
extern "C" __global__ void __closesthit__radiance()
{
const TriangleMeshSBTData &sbtData
= *(const TriangleMeshSBTData*)optixGetSbtDataPointer();
PRD &prd = *getPRD<PRD>();
// ------------------------------------------------------------------
// gather some basic hit information
// ------------------------------------------------------------------
const int primID = optixGetPrimitiveIndex();
const vec3i index = sbtData.index[primID];
const float u = optixGetTriangleBarycentrics().x;
const float v = optixGetTriangleBarycentrics().y;
// ------------------------------------------------------------------
// compute normal, using either shading normal (if avail), or
// geometry normal (fallback)
// ------------------------------------------------------------------
const vec3f &A = sbtData.vertex[index.x];
const vec3f &B = sbtData.vertex[index.y];
const vec3f &C = sbtData.vertex[index.z];
vec3f Ng = cross(B-A,C-A);
vec3f Ns = (sbtData.normal)
? ((1.f-u-v) * sbtData.normal[index.x]
+ u * sbtData.normal[index.y]
+ v * sbtData.normal[index.z])
: Ng;
// ------------------------------------------------------------------
// face-forward and normalize normals
// ------------------------------------------------------------------
const vec3f rayDir = optixGetWorldRayDirection();
if (dot(rayDir,Ng) > 0.f) Ng = -Ng;
Ng = normalize(Ng);
if (dot(Ng,Ns) < 0.f)
Ns -= 2.f*dot(Ng,Ns)*Ng;
Ns = normalize(Ns);
// ------------------------------------------------------------------
// compute diffuse material color, including diffuse texture, if
// available
// ------------------------------------------------------------------
vec3f diffuseColor = sbtData.color;
if (sbtData.hasTexture && sbtData.texcoord) {
const vec2f tc
= (1.f-u-v) * sbtData.texcoord[index.x]
+ u * sbtData.texcoord[index.y]
+ v * sbtData.texcoord[index.z];
vec4f fromTexture = tex2D<float4>(sbtData.texture,tc.x,tc.y);
diffuseColor *= (vec3f)fromTexture;
}
// start with some ambient term
vec3f pixelColor = (0.1f + 0.2f*fabsf(dot(Ns,rayDir)))*diffuseColor;
// ------------------------------------------------------------------
// compute shadow
// ------------------------------------------------------------------
const vec3f surfPos
= (1.f-u-v) * sbtData.vertex[index.x]
+ u * sbtData.vertex[index.y]
+ v * sbtData.vertex[index.z];
const int numLightSamples = NUM_LIGHT_SAMPLES;
for (int lightSampleID=0;lightSampleID<numLightSamples;lightSampleID++) {
// produce random light sample
const vec3f lightPos
= optixLaunchParams.light.origin
+ prd.random() * optixLaunchParams.light.du
+ prd.random() * optixLaunchParams.light.dv;
vec3f lightDir = lightPos - surfPos;
float lightDist = gdt::length(lightDir);
lightDir = normalize(lightDir);
// trace shadow ray:
const float NdotL = dot(lightDir,Ns);
if (NdotL >= 0.f) {
vec3f lightVisibility = 0.f;
// the values we store the PRD pointer in:
uint32_t u0, u1;
packPointer( &lightVisibility, u0, u1 );
optixTrace(optixLaunchParams.traversable,
surfPos + 1e-3f * Ng,
lightDir,
1e-3f, // tmin
lightDist * (1.f-1e-3f), // tmax
0.0f, // rayTime
OptixVisibilityMask( 255 ),
// For shadow rays: skip any/closest hit shaders and terminate on first
// intersection with anything. The miss shader is used to mark if the
// light was visible.
OPTIX_RAY_FLAG_DISABLE_ANYHIT
| OPTIX_RAY_FLAG_TERMINATE_ON_FIRST_HIT
| OPTIX_RAY_FLAG_DISABLE_CLOSESTHIT,
SHADOW_RAY_TYPE, // SBT offset
RAY_TYPE_COUNT, // SBT stride
SHADOW_RAY_TYPE, // missSBTIndex
u0, u1 );
pixelColor
+= lightVisibility
* optixLaunchParams.light.power
* diffuseColor
* (NdotL / (lightDist*lightDist*numLightSamples));
}
}
prd.pixelColor = pixelColor;
}
extern "C" __global__ void __anyhit__radiance()
{ /*! for this simple example, this will remain empty */ }
extern "C" __global__ void __anyhit__shadow()
{ /*! not going to be used */ }
//------------------------------------------------------------------------------
// miss program that gets called for any ray that did not have a
// valid intersection
//
// as with the anyhit/closest hit programs, in this example we only
// need to have _some_ dummy function to set up a valid SBT
// ------------------------------------------------------------------------------
extern "C" __global__ void __miss__radiance()
{
PRD &prd = *getPRD<PRD>();
// set to constant white as background color
prd.pixelColor = vec3f(1.f);
}
extern "C" __global__ void __miss__shadow()
{
// we didn't hit anything, so the light is visible
vec3f &prd = *(vec3f*)getPRD<vec3f>();
prd = vec3f(1.f);
}
先看一下正常光线的ClosestHitShader。前半部分和之前写的几乎一样,唯一的区别就是考虑了法线和光线入射方向是否同向,如果同向就翻转一下法线。
后半部分便开始处理阴影射线。首先我们根据重心坐标,计算得到交点位置的世界坐标,接下来我们开始对光源部分进行多次采样,具体采样方案就是向光源的横纵两个方向做一个随机偏移,然后发射阴影射线进行求交,如果求交失败(进入__miss__shadow)表示可以命中光源,则赋值1;如果求交成功(进入__closesthit__shadow)表示被遮挡,则不赋值。最后着色公式就是:是否(材质色*cos)*可见光源*光强*/(到光源的距离)^2/采样数。
这个公式其实不难理解,首先材质色*cos就是普通的lambert模型,不可见光源就着色0,可见光的话,光源贡献与光强成正比,与距离呈平方反比。最后除以一个采样数表示取平均值。
最后我们改写一下main.cpp,把光源送进去:
SampleWindow(const std::string& title,
const Model* model,
const Camera& camera,
const QuadLight& light,
const float worldScale)
: GLFCameraWindow(title, camera.from, camera.at, camera.up, worldScale),
sample(model, light)
{
sample.setCamera(camera);
}
...
const float light_size = 200.f;
QuadLight light = { /* origin */ vec3f(-1000-light_size,800,-light_size),
/* edge 1 */ vec3f(2.f*light_size,0,0),
/* edge 2 */ vec3f(0,0,2.f*light_size),
/* power */ vec3f(3000000.f) };
// something approximating the scale of the world, so the
// camera knows how much to move for any given user interaction:
const float worldScale = length(model->bounds.span());
SampleWindow *window = new SampleWindow("Optix 7 Course Example",
model,camera,light,worldScale);
好了,然后我就得到了这样的画面:

我人裂开。。。这是什么东西啊,蓝色是哪里来的?
经过检查,发现RayGen shader忘了改了。。。不仅没有多次采样,还没有做溢出处理。。。
struct PRD {
Random random;
vec3f pixelColor;
};
extern "C" __global__ void __raygen__renderFrame()
{
// compute a test pattern based on pixel ID
const int ix = optixGetLaunchIndex().x;
const int iy = optixGetLaunchIndex().y;
const auto& camera = optixLaunchParams.camera;
// our per-ray data for this example. what we initialize it to
// won't matter, since this value will be overwritten by either
// the miss or hit program, anyway
PRD prd;
prd.random.init(ix + accumID * optixLaunchParams.frame.size.x,
iy + accumID * optixLaunchParams.frame.size.y);
prd.pixelColor = vec3f(0.f);
// the values we store the PRD pointer in:
uint32_t u0, u1;
packPointer(&pixelColorPRD, u0, u1);
int numPixelSamples = NUM_PIXEL_SAMPLES;
vec3f pixelColor = 0.f;
for (int sampleID = 0; sampleID < numPixelSamples; sampleID++) {
// normalized screen plane position, in [0,1]^2
const vec2f screen(vec2f(ix + prd.random(), iy + prd.random())
/ vec2f(optixLaunchParams.frame.size));
// generate ray direction
vec3f rayDir = normalize(camera.direction
+ (screen.x - 0.5f) * camera.horizontal
+ (screen.y - 0.5f) * camera.vertical);
optixTrace(optixLaunchParams.traversable,
camera.position,
rayDir,
0.f, // tmin
1e20f, // tmax
0.0f, // rayTime
OptixVisibilityMask(255),
OPTIX_RAY_FLAG_DISABLE_ANYHIT,//OPTIX_RAY_FLAG_NONE,
RADIANCE_RAY_TYPE, // SBT offset
RAY_TYPE_COUNT, // SBT stride
RADIANCE_RAY_TYPE, // missSBTIndex
u0, u1);
pixelColor += prd.pixelColor;
}
const int r = int(255.99f * min(pixelColor.x / numPixelSamples, 1.f));
const int g = int(255.99f * min(pixelColor.y / numPixelSamples, 1.f));
const int b = int(255.99f * min(pixelColor.z / numPixelSamples, 1.f));
// convert to 32-bit rgba value (we explicitly set alpha to 0xff
// to make stb_image_write happy ...
const uint32_t rgba = 0xff000000
| (r << 0) | (g << 8) | (b << 16);
// and write to frame buffer ...
const uint32_t fbIndex = ix + iy * optixLaunchParams.frame.size.x;
optixLaunchParams.frame.colorBuffer[fbIndex] = rgba;
}
这里就是说,我们场景需要多次采样,最后取一个平均值。因为光强非常高,因此算出来的最终像素颜色很有可能会溢出,所以需要做一个钳制。

感觉还凑合吧。反正光线一共只弹射两次,相当于牺牲画面追求效率了。
Part4. 时域降噪
在之前我们写离线光追的时候,每次渲染,都会跑N次采样,直到每个像素都完成N次采样后,再统一把结果写入最终图像,交给客户端。但如今我们希望实现实时光追,所以渲染器不能等每个像素都采够N次后才提交结果,这样屏幕的刷新频率太低了。所以我们需要每次都只采样N/k次,然后立刻更新图像提交结果,表示这一帧的任务完成。提交完以后开始新的渲染,再与前面的帧的结果进行混合,这样持续刷新k帧,就能得到最终的光追结果了。
举个例子,假如一个场景的采样数是20000次,基本上交给optix也需要10几秒才能采完,这也意味着用户每次更新视角,optix都需要花10几秒才能更新一次画面,这样有违背“实时”的理念。因此我们每次只让optix采样100次就刷新结果,然后让客户端取到当前的画面,此时再让optix进行新一次100采样数的渲染,每次渲染结果都与之前的结果做平均,这样用户就可以看到能实时刷新画面、噪点由多变少的实时场景了,这就是时域上的降噪。并且可以计算得到,只需200帧,就可以看到光追完成的最终结果了。
当然这也意味着,当用户切换视角时,前几帧会有铺天盖地的噪点。为了让用户有一个好的观感,所以还需要空域降噪的功能,空域降噪可以让用户在切换视角后前几帧不用看到很扎眼的噪声图,而是场景的一个模糊的外观。当然这一节先不讲空域降噪,放在后面说。
int numPixelSamples = 1;
struct {
int frameID = 0;
float4 *colorBuffer;
/*! the size of the frame buffer to render */
vec2i size;
} frame;
这里使用numPixelSamples控制每一帧的采样数;用frameID控制当前是渲染的第几帧。
然后我们修改一下SampleRenderer.cpp的render函数:
void SampleRenderer::render()
{
launchParamsBuffer.upload(&launchParams, 1);
launchParams.frame.frameID++;
每次调用渲染接口时,就给frameID加1,表示下一帧的渲染。
接下来我们编写一下着色器,只需要修改一下RayGen Shader:
extern "C" __global__ void __raygen__renderFrame()
{
// compute a test pattern based on pixel ID
const int ix = optixGetLaunchIndex().x;
const int iy = optixGetLaunchIndex().y;
const auto& camera = optixLaunchParams.camera;
PRD prd;
prd.random.init(ix + optixLaunchParams.frame.size.x * iy,
optixLaunchParams.frame.frameID);
prd.pixelColor = vec3f(0.f);
// the values we store the PRD pointer in:
uint32_t u0, u1;
packPointer(&prd, u0, u1);
int numPixelSamples = optixLaunchParams.numPixelSamples;
vec3f pixelColor = 0.f;
for (int sampleID = 0; sampleID < numPixelSamples; sampleID++) {
// normalized screen plane position, in [0,1]^2
vec2f screen(vec2f(ix + prd.random(), iy + prd.random())
/ vec2f(optixLaunchParams.frame.size));
// generate ray direction
vec3f rayDir = normalize(camera.direction
+ (screen.x - 0.5f) * camera.horizontal
+ (screen.y - 0.5f) * camera.vertical);
optixTrace(optixLaunchParams.traversable,
camera.position,
rayDir,
0.f, // tmin
1e20f, // tmax
0.0f, // rayTime
OptixVisibilityMask(255),
OPTIX_RAY_FLAG_DISABLE_ANYHIT,//OPTIX_RAY_FLAG_NONE,
RADIANCE_RAY_TYPE, // SBT offset
RAY_TYPE_COUNT, // SBT stride
RADIANCE_RAY_TYPE, // missSBTIndex
u0, u1);
pixelColor += prd.pixelColor;
}
vec4f rgba(pixelColor / numPixelSamples, 1.f);
// and write/accumulate to frame buffer ...
const uint32_t fbIndex = ix + iy * optixLaunchParams.frame.size.x;
if (optixLaunchParams.frame.frameID > 0) {
rgba
+= float(optixLaunchParams.frame.frameID)
* vec4f(optixLaunchParams.frame.colorBuffer[fbIndex]);
rgba /= (optixLaunchParams.frame.frameID + 1.f);
}
optixLaunchParams.frame.colorBuffer[fbIndex] = (float4)rgba;
}
前半部分基本没有变化,后面就是根据numPixelSamples决定采样数,然后根据帧号,去和之前的帧进行混合,从而实现随着时间推进噪点减少的效果。
当我们切换视角的时候,前面的帧就需要丢弃了,此时从第0帧开始渲染。所以我们在SampleRenderer.cpp的setCamera()函数中添加一句launchParams.frame.frameID = 0;最后在draw函数中,将glfwindow的画面纹理格式改成GLenum texelType = GL_FLOAT,不然会花屏。

到这里,我们前10个example就彻底完成了。当然后面还有两个example是讲降噪的,不过我个人感觉目前不着急复现这个。现在最大的问题是:我们的场景到目前依旧只有2次弹射,尤其第二次还是定向弹射,这与真正的光追差距还很大。因此下节开始,我们正式脱离教程,开始实现真正的光线追踪。





