首先需要查看本博客的这篇文章:【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平台再编译运行: