预览图像只是蒙特卡罗渲染中的第一轮样本(Arnold 就是这样)。这种“开始嘈杂”,然后“提高质量”不一定是预期的功能。它存在于所有蒙特卡罗渲染器中,因为执行无偏采样的性质意味着您从一些样本开始,其中许多可能不准确(在最终图像中产生噪声)。然后,随着越来越多的样本被发射到场景中(对于每个像素),结果最终将收敛于预期的结果(噪声将减少,不准确的样本贡献越来越少)。
Monte Carlo 渲染将永远进行渲染,但是在一定数量的样本之后,每个贡献都会很小,因此被忽略(确定实际结果)。这就是为什么图像开始有噪声(样本不多,并且有大量不准确的样本),然后随着越来越多的样本用于估计像素颜色而逐渐提高其质量。
渐进式采样是另一种优化,旨在减少收敛到结果所需的时间。即如前所述,在可能贡献更多的区域中发射样本(即采样像素时差异更大,需要更高的精度,因此为该像素计算更多样本)。
P.S 继续关注 Scratch-a-pixel。这是一个很好的资源。
P.P.S 此外,OpenGL 可用于辅助纹理/图像分析(决定对哪些像素进行更多采样等),或用于通过将几何图形绘制到屏幕外缓冲区来加速相交测试(只是我使用过的两种方式过去)。然而,这将取决于实施。默认情况下,OpenGL 不提供任何光线追踪系统所需的内容。
关于显示您的渲染。
首先创建一个与您需要的输出图像尺寸相同的帧缓冲区。这将有效地成为未压缩的 RGB24 或 RGBA32 图像。使用与您所需的显示输出相关的格式(因此可以在有限的延迟下完成复制,并且直接显示不需要转换/处理)。它还将包括另一位元信息,每个像素跟踪像素当前使用的样本数量。这允许结果填充彼此互斥的帧缓冲区。也就是说,您可以在需要的区域中触发更多像素(自适应),并在需要时选择呈现帧缓冲区的内容,同时继续在相同的渲染上下文中对像素进行采样(渐进式)。
应该在主循环的每个循环中保留此帧缓冲区,以便将每个循环的结果累积到帧缓冲区中。计算单个像素的结果,通常是该像素的所有样本的总和除以样本的总数(其他采样方法可能会相应地对样本进行加权),用于像素的标准抖动网格采样。
要向用户呈现此图像,这取决于您使用的 api,但您只需要以与显示图像/位图相同的方式显示帧缓冲区。我个人的做法:
在 openGL 中绘制一个带纹理的四边形,使用帧缓冲区作为纹理(因此您需要每帧使用帧缓冲区的内容更新纹理)。
使用 windows gdi 将 DIB 位图呈现给控件。
输出为未压缩的图像格式(这可以快速完成二进制 PPM,或用于直接复制帧缓冲区内容的 TGA/tiff/未压缩位图)或压缩图像,例如 png 或 jpg。
这里有一些代码,具体实现取决于您选择使用的 api,但希望这是伪代码,足以详细描述正在发生的事情。这是c-esque。
声明/定义。
// 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];
然后在你的 init 方法中。为了初始化帧缓冲区,这会将其设置为黑色图像(很重要,因为我们要将第一个样本添加到 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;
}
...
然后在主循环/循环中。
// 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 来显示帧缓冲区。
这有望展示如何编写一个系统,该系统将随着计算的越来越多的细节逐渐显示图像的内容。它适用于任何形式的光线追踪(如前所述,鉴于模拟的性质,蒙特卡洛会产生噪声,有偏差的渲染可能会或可能不会取决于它们的工作方式。通常它不会比图像抗锯齿更多,尽管有偏差的渲染可能会出现噪点)。