路径追踪器的简单渐进式渲染 Window

Simple Progressive Rendering Window for a Path Tracer

希望每个人都在 great.Been 考虑一段时间,出于好奇,而不是 anything.Been 使用 Maya 和 Arnold 一段时间 now.Just 作为业余爱好的东西,主要是简单的渲染与我的路径进行比较 tracer.I 意识到他们的渲染器具有这个非常好的功能,可以让您在渲染时看到图像.... Progressively.It 似乎从较低的采样率和 aa 量开始,然后重新在增加这些参数时渲染图像 automatically.I 认为这真的是 cool.And 一种在渲染达到最大值之前显示渲染预览的好方法 quality.It 让我非常有兴趣为我做同样的事情我正在工作的路径跟踪器 on.Currently 它等待整个渲染完成,然后在您的驱动器上保存一个简单的 ppm 文件。 我现在的问题是.....有谁知道如何做这样的事情?我已尽力找出答案,我得到的唯一信息是涉及 OpenGL somehow.I我不想创建与 Maya.Just 相同的东西,一个简单的 window 弹出随着渲染开始并逐渐使图像更好。 再次.....这比任何东西都更令人好奇 else.As 因为我认为它真的很酷 谢谢:)

它不以任何方式限制于 OpenGL。您将渲染器设计为 运行 在单独的线程(可能是多个线程甚至是多台机器)中,并逐步将部分结果发送到主线程。然后主线程创建一个 window 来显示这些结果。这里没有魔法。

预览图像只是 monte carlo 渲染(Arnold 渲染)中的第一轮样本。这个 'start off noisy',然后 'improve quality' 不一定是预期的功能。它存在于所有 monte carlo 渲染器中,因为执行无偏采样的性质意味着您从一些样本开始,其中许多样本可能不准确(在最终图像中产生噪声)。然后,随着越来越多的样本被发射到场景中(对于每个像素),结果最终将收敛于预期结果(噪声将减少,不准确的样本贡献越来越小)。

Monte Carlo renders 将永远进行渲染,但是在一定数量的样本之后,每个贡献都将是次要的,因此被忽略(以实际结果为准)。这就是为什么图像开始有噪声(样本不多,样本数量多)然后随着越来越多的样本用于估计像素颜色而逐渐提高其质量。

渐进式采样是另一种优化,旨在减少收敛于结果所需的时间。即如前所述,在可能贡献更多的区域发射样本(即采样像素时差异更大,需要更高的精度,因此为该像素计算更多样本)。

P.S 继续看 Scratch-a-pixel。这是一个很好的资源。

P.P.S OpenGL 还可用于帮助 texture/image 分析(决定对哪些像素进行更多采样等),或用于通过将几何图形绘制到屏幕外来加速相交测试缓冲区(只是我过去使用它的两种方式)。然而,这将取决于实施。默认情况下,OpenGL 不提供任何光线追踪系统所需的任何东西。

关于显示渲染。

首先创建一个与您需要的输出图像尺寸相同的帧缓冲区。这实际上是未压缩的 RGB24 或 RGBA32 图像。使用与您所需的显示输出相关的格式(因此可以在有限的延迟下完成复制,直接显示不需要 conversion/processing)。 Is 还将包含另一位元信息,每个像素跟踪该像素当前使用的样本数。这允许结果相互排斥地填充帧缓冲区。也就是说,您可以在需要的区域(自适应)触发更多像素,并选择在需要时显示帧缓冲区的内容,同时继续在同一渲染上下文中对像素进行采样(渐进式)。

此帧缓冲区应在主循环的每个循环中保留,以便每个循环的结果都累积到帧缓冲区中。计算单个像素的结果,通常是该像素所有样本的总和除以样本总数(其他采样方法可能会相应地加权样本),用于像素的标准抖动网格采样。

要向用户显示此图像,这取决于您使用什么 api,但是您需要做的就是以相同的方式显示帧缓冲区将显示一个 image/bitmap。我亲自完成的方法:

  • 在openGL中绘制一个带纹理的四边形,使用帧缓冲区作为纹理(所以你需要用帧缓冲区的内容更新每帧的纹理)。

  • 使用 windows gdi 将 DIB 位图渲染到控件。

  • 输出为未压缩的图像格式(这可以快速完成二进制 PPM,或 TGA/tiff/uncompressed 用于直接复制帧缓冲区内容的位图)或压缩图像,例如 png 或jpg.

这是一些代码,实现取决于您选择使用哪个 api,但希望这是伪造的,足以详细描述正在发生的事情。很c-esque.

Declarations/Definitions.

// number of pixels in the resultant final render image.
unsigned int numberOfPixels = imageWidth * imageHeight;

// number of channels of ouput image (also ray result) RGB 3 channels.
unsiged int channelSize = 3; 

// RGB pixel. Each channel/colour component is in the range 0 <= ... <= 1
struct pixel
{
    float red;
    float green;
    float blue;
};

// framebuffer, 3 channels RGB
pixel frameBuffer[numberOfPixels];

// framebuffer meta data. Number of samples for each pixel.
int pixelSampleCount[numberOfPixels];

然后在你的初始化方法中。要初始化帧缓冲区,将其设置为黑色图像(这很重要,因为我们要将第一个样本添加到 0,0,0)。

// your init routine
...   

    for (unsiged int p = 0; p < numberOfPixels; ++p )
    {
        // initialise the framebuffer to black (0,0,0)
        frameBuffer[p].red = 0.0;
        frameBuffer[p].green = 0.0;
        frameBuffer[p].blue = 0.0;

        // set the sample count to 0
        pixelSampleCount[p] = 0;
    }

...

然后在主要loop/cycle。

// your main loop
...

// Main loop...each cycle we will cast a single sample for each pixel. Of course you can get as many sample results as you want if you
// intelligently manage the casting (adaptive), ensure each cast knows which pixel it is contributing to, so when it comes to accumulation of
// this sample result, it can be amended to the correct pixel and the correct sample count incremented as you add to the framebuffer.

for ( unsigned int x = 0; x < imageWidth; ++x )
{
    for ( unsigned int y = 0; y < imageHeight; ++y )
    {
         // get the result of the sample for this pixel (e.g cast the ray for this pixel, jittered according to sampling method). Ultimately
         // each sample needs to be different (preferably unique and random) from the previous cycle and will return a different result.
         pixel castResult = GetSampleResult(x, y, ... );    // aka cast the ray and get the resultant 'colour'

         // Get the current pixel from the frame buffer read to ammend it with the new sample/contribution. 
         unsigned int currentPixelIndex = (y * imageWidth) + x;
         pixel& pixelOfSample = frameBuffer[currentPixelIndex];

         // to correctly accumulate this sample, we must first multiply (scale up) each colour component
         // by the number of samples/contributions to this pixel. We can then add the sample result and divide
         // (scale down) the result (sum of all samples now) by the new number of samples.
         pixelOfSample.red = ( (pixelOfSample.red * pixelSampleCount[currentPixelIndex]) + castResult.red ) / ( pixelSampleCount[currentPixelIndex] + 1 );

        // repeat this for the rest of the components in the pixel, i.e for green and blue in this case.
        pixelOfSample.green = ( (pixelOfSample.green * pixelSampleCount[currentPixelIndex]) + castResult.green ) / ( pixelSampleCount[currentPixelIndex] + 1 );
        pixelOfSample.blue = ( (pixelOfSample.blue * pixelSampleCount[currentPixelIndex]) + castResult.blue ) / ( pixelSampleCount[currentPixelIndex] + 1 );

          // increment the sample count for this pixel.
          ++pixelSampleCount[currentPixelIndex];
     }
}

// And then send this to your output gdi/opengl/image output etc.
// For displaying direct in gdi, use BitBlt(...) with SRCCOPY.
// For displaying in OpenGL use glTexture2D(...)

glTexture2D(...);    // for OpenGL
BitBlt(...);         // for win gdi

// The next loop you can simply display the framebuffer (it would look the same as previous cycle) or you can fire a load of rays and then add this to your framebuffer and display that, giving you a different display.

...

N.B GL 和 gdi 都以不同的方式期望图像。因此,您可能需要水平翻转图像以获得正确的方向。这取决于您如何在内部存储帧缓冲区以及您使用哪个 api 来显示帧缓冲区。

这有望展示如何编写一个系统,随着为图像计算越来越多的细节,该系统将逐步显示图像的内容。它适用于任何形式的光线追踪(如前所述,考虑到模拟的性质,monte carlo 会产生噪音,有偏差的渲染可能会或可能不会取决于它们的工作方式。通常它不会比抗锯齿多很多图像,尽管有偏差的渲染可能会出现噪点)。