Recent Posts

人体速写笔记


色彩光影理论



Stay Connected

A smart wordpress theme for bloggers
Optix光线追踪渲染器(三)复杂模型纹理、简单阴影与时域降噪

Optix光线追踪渲染器(三)复杂模型纹理、简单阴影与时域降噪

引言

在上一篇文章中,我们完成了多模型多材质场景的渲染,此时我们已经可以自由地为场景添加很多模型了。但是一个场景如果只有简单的方块形状,而且没有图案,那渲染出来的效果可谓不堪入目,实在对不起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次弹射,尤其第二次还是定向弹射,这与真正的光追差距还很大。因此下节开始,我们正式脱离教程,开始实现真正的光线追踪。

Leave a reply

Your email address will not be published. Required fields are marked *

Stay Connected

Instagram Feed

Recent Posts

人体速写笔记


色彩光影理论



×