【OptiX】第0个示例 OptixHello 学习Optix的工程配置以及基本框架

首先需要查看本博客的这篇文章:【Optix】Optix介绍与示例编译 把该安装的工程都安装好。可以按照本文所说的顺序创建和理解代码,也可以在本文末尾下载到已经配置好的代码。建议首先在本文末尾处下载代码,编译通过,这样配合文件看心情会舒畅一些。再一步一步的建内心也不慌乱。

首先来看本文的输出效果。

【建工程】

打开VS2015,新建工程取名为OptiXHello,按照编程习惯,路径中不要有中文。尽量也不要有空格。

选择Win32控制台应用程序,应用程序设置中选空项目

默认情况下是X86程序,注意这里很多配置都有选择X86还是X64,是DEBUG还是RELEASE,这里拿X64来示例。首先把当前编译环境修改为X64:

其次我们要新建一个main.cpp的源文件:并编写以下代码:

#include <iostream>

int main()
{
    std::cout << "Hello World" << std::endl;
    return 0;
}

确保可以正常运行后,配置工程环境,选菜单->项目->属性,在配置->VC++目录中,右侧的包含目录中新增CUDA和OPTIX的包含目录:注意当前配置是DEBUG平台是X64

C:\ProgramData\NVIDIA Corporation\OptiX SDK 6.0.0\include;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.1\include;D:\9.PRO\rtx\optix\OptiX SDK 6.0.0\SDK\support\freeglut\include;$(ProjectDir)

为什么要加$(ProjectDir)呢,是因为后面我们要把glew进行编译,而glew.c的包含使用的是#include <gl/glew.h>,使用的是<>而不是"",若是#include "gl/glew.h"则会在当前工程目录下寻找,而使用<>只会在上面配置的目录中寻找。$(ProjectDir)就是当前目录,意思是把当前目录加入到包含目录当中。

在链接器->输入->附加依赖项中新增如下依赖:

C:\ProgramData\NVIDIA Corporation\OptiX SDK 6.0.0\lib64\optix.6.0.0.lib;C:\ProgramData\NVIDIA Corporation\OptiX SDK 6.0.0\SDK\support\freeglut\win64\Release\freeglut.lib;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.1\lib\x64\nvrtc.lib;

因为代码中用到了glew,需要将glew拷贝到工程中,glew在SDK中的位置是:

C:\ProgramData\NVIDIA Corporation\OptiX SDK 6.0.0\SDK\sutil

涉及文件

将其拷贝到与main.cpp同等目录下。将其加入到工程文件中,.h与.c文件都加入。

因为编译glew需要预定义一些宏,项目->属性->C/C++->预处理器定义:

_DEBUG;_CONSOLE;WIN32;GLEW_BUILD;GLUT_FOUND;GLUT_NO_LIB_PRAGMA;

【写主机端代码】

因为OptiX涉及到GPU端CUDA等操作,各种初始化操作等依赖较多,因此容易抛出异常,在main中首先将结构修改为try catch结构:

int main(int argc, char* argv[])
{
    try
    {

    }
    catch (const std::exception& e)
    {
        std::cerr << "OptiX Error: '" << e.what() << "'\n";
        exit(1);
    }
    return 0;
}

本节的代码我们要使用OptiX渲染输出一个缓存,然后使用glut来展示这个缓存。因此这个输出缓存就是二者之间沟通的桥梁。而OptiX要 输出这个简单的缓存首先要弄清楚一些概念:

Context(上下文)

OptiX使用上下文来管理整个渲染场景,比如发射光线的种类,缓存的关联,场景的关联,以及光线生成shader的关联等等。可以理想Context是连接一切的基础,是一切的操作场所。通过Context可以获取程序中所有的配置也可以进行所有的配置。后面逐步介绍。首先申请全局变量Context:

#include <optix.h>
#include <optixu/optixpp_namespace.h>

//申请场景上下文
optix::Context context;

Buffer(缓存)

缓存用来存放纹理、顶点以及其它任何的数值。此时我们创建一个缓存,用来输出OptiX渲染结果输送到glut当中。

//确定场景的长宽
int width = 800, height = 600;
//申请场景上下文
optix::Context context;
optix::Buffer output_buffer;

我们还定义了width和height用来确定场景的长和宽。

【光线产生模块(RayGenerationProgram)】

有了上下文和buffer之后,我们要开始介绍入口shader。在Optix中叫做RayGenerationProgram(光线产生模块),由该模块来产生光线并确定光线与场景相交的结果。这个模块是一个shader针对要渲染的结果的width和height每个像素产生一个调用,使用一个变量叫做rtLaunchIndex的来记录是哪个像素,也即rtLaunchIndex是个二维的整型内置变量,除了计算包围盒模块(BoundingBoxProgram)外,存在于整个渲染过程当中。也就是都可以访问到该变量。

光线产生模块使用shader定义,语法是C++的语法,语法简单,只是有一些内置变量和流程需要熟悉。随着本博客不断深入的介绍,用户会对每个内置变量和函数以及流程有着深切的认识。

我们边做边来解释,首先像新建main.cpp一样新建项,只是名字叫做draw_color.cu,内容如下:

#include <optix.h>
#include <optixu/optixu_math_namespace.h>

using namespace optix;

rtDeclareVariable(uint2, launch_index, rtLaunchIndex, );
rtBuffer<float4, 2> result_buffer;

rtDeclareVariable(float3, draw_color, , );

RT_PROGRAM void draw_solid_color()
{
    //rtPrintf("-%f-%f-%f", draw_color.x, draw_color.y, draw_color.z);
    //result_buffer[launch_index] = make_float4(draw_color, 0.f);
    float2 degree = make_float2(launch_index)*3.14/180.0f;
    result_buffer[launch_index] = make_float4(sin(degree.x), cos(degree.y), 0.0, 0.f);
}

在工程中要想把cu文件识别为C++文件,则需要一个设置,菜单->工具->选项->文本编辑器->文件扩展名,扩展名输入cu,编辑器选择Microsoft Visual C++。点击确定即可。

下面对draw_color.cu中的每句话做出解释:

包含文件就不说了,下面解释rtDeclareVariable(a, b, c, d),其共有四个参数,第一个参数是数据类型,第二个参数是变量名,第三个参数叫做名字空间,一来是为了防止重名,二来是为了说明该变量是在该阶段使用,第四参数是注释,使用字符串,可以随便写。

那么着重来说一下第三个参数:名字空间。OptiX目前共有5个名字空间,如下:

该图在随安装包的文档《Programming Guide》的第43页。这五个名字空间分别在不同的渲染阶段起作用代表不同的含义。就rtLaunchIndex来说比较特殊,在shader中只有对该变量读取的权限,rtDeclareVariable相当于定义了一个引用实例而已。其是二维的存放的是像素的行列编号,该编号也用于定位缓存中的位置。

rtBuffer<float4, 2> result_buffer 该行在shader中申明了一个buffer,也就是在GPU侧申请了一个buffer。且该buffer是float4类型,有二维。其实这个buffer就是输出buffer,需要向CPU端进行传递。在CPU端使用语句:rtContextDeclareVariable( context, "result_buffer", &output_buffer) 语句或者context["result_buffer"]->set( output_buffer);与CPU端的buffer进行相连。

rtDeclareVariable(float3, draw_color, , ); 就是申请了一个CPU与GPU端共同的变量draw_color,该变量只读,不可改变。若要改变可以创建一个临时变量拷贝后改变。这种申请的变量是全局变量,所有的shader的阶段都可读取。

RT_PROGRAM void draw_solid_color()
{
    result_buffer[launch_index] = make_float4(draw_color, 0.f);
}

每个光线发生模块需要有一个启动函数,在主机端使用

    Program ray_gen_program = context->createProgramFromPTXString( ptx, "draw_solid_color" );
    context->setRayGenerationProgram( 0, ray_gen_program );

来进行设置。该函数就是启动函数,该启动函数里只有一句话就是把输出buffer里对应像素位置设置成draw_color的值。整个光线跟踪流程就结束了。可以看到没有光线,其实即便有光线也是为了输出结果到result_buffer中,后面我们会逐渐的介绍各种shader,最终的计算都是为了填允result_buffer。因此这是一个大的框架。

【构建context和buffer】

有了上面的光线发生模块的shader,我们来初始化context和输出buffer:

        context = optix::Context::create();
        context->setRayTypeCount(1);//只有1种光线
        context->setEntryPointCount(1);//只有1个光线发生相机

        //申请Buffer
        output_buffer = context->createBuffer(RT_BUFFER_OUTPUT, RT_FORMAT_FLOAT4, width, height);
        context["result_buffer"]->set(output_buffer);

        //对shader进行编译,并取出编译结果
        const char* ptx = getPtxString("draw_color.cu");
        //从ptx字串中标识 "draw_solid_color"入口函数,并创建并返回光线发生器模块
        optix::Program ray_gen_program = context->createProgramFromPTXString(ptx, "draw_solid_color");
        //在setEntryPointCount设置只有一个入口,即指于此
        context->setRayGenerationProgram(0, ray_gen_program);
        //设置shader中变量draw_color
        context["draw_color"]->setFloat(1.0f, 0.0f, 1.0f);  

        context->setPrintEnabled(true);
        context->setPrintBufferSize(4096);

        //内部检查有效性
        context->validate();
        //开启第0个入口
        context->launch(0, width, height); 

上面的代码完成了申请context与设置光线种类,比如有时候一个入口会发两种光线,一种用于渲染,一种专门用于计算阴影等。申请buffer的语句是使用context的createBuffer参数,可以看出这里申请的是一个output buffer,用于GPU向CPU侧输出的。可以通过‘|’符号设置如下标识:

typedef enum
{
  RT_BUFFER_INPUT                = 0x1,                               /*!< Input buffer for the GPU          */
  RT_BUFFER_OUTPUT               = 0x2,                               /*!< Output buffer for the GPU         */
  RT_BUFFER_INPUT_OUTPUT         = RT_BUFFER_INPUT | RT_BUFFER_OUTPUT,/*!< Ouput/Input buffer for the GPU    */
  RT_BUFFER_PROGRESSIVE_STREAM   = 0x10,                              /*!< Progressive stream buffer         */
} RTbuffertype;

/*! Buffer flags */
typedef enum
{
  RT_BUFFER_GPU_LOCAL            = 0x4,  /*!< An @ref RT_BUFFER_INPUT_OUTPUT has separate copies on each device that are not synchronized                               */
  RT_BUFFER_COPY_ON_DIRTY        = 0x8,  /*!< A CUDA Interop buffer will only be synchronized across devices when dirtied by @ref rtBufferMap or @ref rtBufferMarkDirty */
  RT_BUFFER_DISCARD_HOST_MEMORY  = 0x20, /*!< An @ref RT_BUFFER_INPUT for which a synchronize is forced on unmapping from host and the host memory is freed */
  RT_BUFFER_LAYERED              = 0x200000, /*!< Depth specifies the number of layers, not the depth of a 3D array */
  RT_BUFFER_CUBEMAP              = 0x400000, /*!< Enables creation of cubemaps. If this flag is set, Width must be equal to Height, and Depth must be six. If the @ref RT_BUFFER_LAYERED flag is also set, then Depth must be a multiple of six */
} RTbufferflag;

可以看到这些宏以及注释都能看到其中的含义。

在shade中的变量可以通过

context["result_buffer"]->set(output_buffer);

context["draw_color"]->setFloat(1.0f, 0.0f, 1.0f);

的语句进行设置。当执行context->launch时,则整个shader就会启动。这里要特别说明一下

        context->setPrintEnabled(true);
        context->setPrintBufferSize(4096);

这里是允许shade端出打印,配合shade中的tfPrintf来使用,语法和C语言的printf是一样的。当上面两句设置时,shade中的打印才会输出。注意由于是并行,shade中的打印输出的前后顺序是不确定的。类似于多线程。

【展示buffer】

当我们完成了对OptiX的主要设置与渲染之后,就剩下将输出缓存output_buffer展示在OPENGL当中了,关于OPENGL我们使用freeglut来简化操作,freeglut初始化了一个小窗口,并且在上面贴一张纹理,这里无特别奇特之处,但是尤其把output_buffer中的内容输入到gl中的texture2d当中需要特别说明一下。

首先要注意颜色空间,有些从[0, 255]代表黑白,有些从[0,1]代表黑白。本例我们有浮点数,一旦有浮点数就可以认为不是整数,认为是[0, 1]代表黑白。这在opengl中是sRGB颜色空间,因此需要使用下面的语句启用sRGB颜色空间:

    //假如是浮点数,则证明在SRGB颜色空间,也即每个颜色[0,1],与之对应的是[0, 255]
    GLboolean use_SRGB = GL_FALSE;

    glGetBooleanv(GL_FRAMEBUFFER_SRGB_CAPABLE_EXT, &use_SRGB);
    if (use_SRGB)
        glEnable(GL_FRAMEBUFFER_SRGB_EXT);

其次将OptiX buffer中的内容传递到主机端(CPU)有个简便的方法

    GLvoid* imageData = 0;
    imageData = output_buffer->map(0, RT_BUFFER_MAP_READ);

使用map方法则代表将buffer中的内容传送到了imageData当中。而从主机端将内容传送到服务端则可以使用unmap,可以先把数据拷贝到buffer中再unmap,拷贝是使用 memcpy( vertex_buffer->map(), tet.vertices, sizeof( tet.vertices ) );类似即可。

当执行完map后,数据就到了imageData当中,因为是float类型的RGBA,用户可以自己打印查看数据是否正确。

【代码下载】

链接:https://pan.baidu.com/s/1LpFdVgvCtk1f7WMhdp35Lg 
提取码:6vod 
下载解压后使用VS2015打开工程,并确保把工程调整至Debug和X64平台再编译运行:

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值