주의: 이미지가 굉장히 작게 나오니 원문을 참고하자.
이번 주제는 Antialiasing이다. 백문이 불여일견이라고 먼저 원문의 7단원 가장 마지막에 위치한 이미지인
Image 6: Before and after antialiasing
를 먼저 살펴보자. 이미지를 살펴봤다면 Antialiasing 전후의 이미지가 다르다는 건 알수있다. Antialiasing를 적용하지 않은 이미지의 경우 어떤 물체와 배경이나 다른 사물 사이의 경계에서 일어나는 계단현상(Aliasing)이 일어난다. 이런 계단현상을 매끈하게 해 이미지를 보정하는 역할을 하는 게 Antialiasing이다.
원리는 계단현상이 일어나는 경계부분에 원래 칠해질 색상 대신에 해당 지점 부근의 색상값의 평균을 계산해 근방의 색들의 평균값에 해당하는 색을 칠해 주는 것이다. 그러면 Image 6에서 처럼 Antialiasing이 적용된 이미지처럼 이미지가 생성되 이미지의 거친 부분을 완화시켜준다.
Antialasing에 앞서 0과 1사이의 난수를 반환하는 함수를 작성한다. 여기서 주의할 점은 rand()
가 매번 다른 반환을 하진 않는 다는 점을 알아두자.
#include <cstdlib>
...
inline double random_double() {
// Returns a random real in [0,1).
return rand() / (RAND_MAX + 1.0);
}
inline double random_double(double min, double max) {
// Returns a random real in [min,max).
return min + (max-min)*random_double();
}
아래와 같이 짤 수도 있다.
#include <random>
inline double random_double() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}
지금까지 한 픽셀의 색상을 결정하기 위해 광선 하나만 쐈다. Image 6에서 중앙의 작은 구의 가장자리에 계단현상이 일어나는 픽셀을 보자. 해당지점은 광선이 구와 교차했기 때문에 해당 픽셀은 구의 색으로 칠했다. Antialiasing은 단순히 구의 색상이 칠해지는 것이 아니라 그 주변 색상을 고려한 평균값을 구해 대응하는 색상으로 칠해준다.
그럼 어떻게 평균을 구할까?
1. 위에서 만든 난수함수를 사용해 이전까지 쏜 광선을 무작위로 수정해서 samples_per_pixel
회수 만큼 더 쏜다.
2. 무작위로 생성되는 광선은 픽셀의 근처의 값(sample)을 추적하여 평균을 계산한다.
Antialiasing에 앞서 camera class를 작성한다. main.cc
에 있던 내용들을 따로 정리했다고 생각하자.
#ifndef CAMERA_H
#define CAMERA_H
#include "rtweekend.h"
class camera {
public:
camera() {
auto aspect_ratio = 16.0 / 9.0;
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
origin = point3(0, 0, 0);
horizontal = vec3(viewport_width, 0.0, 0.0);
vertical = vec3(0.0, viewport_height, 0.0);
lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};
#endif
색상값이 엉뚱한 값이 되지 않게 0과 1사이의 값이도록 고정하는 함수를 작성한다.
inline double clamp(double x, double min, double max) {
if (x < min) return min;
if (x > max) return max;
return x;
}
매개변수로 받는 무작위 sample의 색상들의 평균을 계산해서 출력하는 함수를 작성한다.
void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
// Divide the color by the number of samples.
auto scale = 1.0 / samples_per_pixel;
r *= scale;
g *= scale;
b *= scale;
// Write the translated [0,255] value of each color component.
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
#include "camera.h"
...
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
// World
hittable_list world;
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
// Camera
camera cam;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
color pixel_color(0, 0, 0);
// 하나의 픽셀에 대해 근처의 색상의 평균을 구한다.
for (int s = 0; s < samples_per_pixel; ++s) {
// 무작위 sample을 samples_per_pixel횟수만큼 뽑는다.
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
// sample의 색상들의 합을 구한다.
pixel_color += ray_color(r, world);
}
// sample의 색상값의 합을 넘겨주고 평균 색상값을 출력한다.
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}