Defocus Blur
이제 마지막 기능인 초점 흐림(defocus blur)을 구현해보겠습니다. 참고로 사진작가들은 이것을 피사계 심도(depth of field)라고 부르니, 레이트레이싱 동료들 사이에서는 초점 흐림이라는 용어를 사용하세요.
실제 카메라에 초점 흐림이 발생하는 이유는 빛을 모으기 위해 (핀홀이 아닌) 큰 구멍이 필요하기 때문입니다. 큰 구멍은 모든 것의 초점을 흐리게 만들지만, 필름/센서 앞에 렌즈를 배치하면 모든 것이 초점이 맞는 특정 거리가 생깁니다. 그 거리에 배치된 물체는 초점이 맞게 보이고, 그 거리에서 멀어질수록 선형적으로 더 흐릿하게 보입니다. 렌즈를 이렇게 생각할 수 있습니다: 초점 거리에 있는 특정 점에서 나와 렌즈에 닿는 모든 광선은 이미지 센서의 한 점으로 다시 굴절됩니다.
카메라 중심과 모든 것이 완벽하게 초점이 맞는 평면 사이의 거리를 초점 거리(focus distance)라고 합니다. 초점 거리는 일반적으로 초점 길이(focal length)와 같지 않다는 점에 유의하세요. 초점 길이는 카메라 중심과 이미지 평면 사이의 거리입니다. 하지만 우리 모델에서는 픽셀 그리드를 초점 평면에 바로 배치할 것이므로, 이 두 값이 동일한 값을 가지게 됩니다. 초점 평면은 카메라 중심에서 초점 거리만큼 떨어져 있습니다.
실제 카메라에서 초점 거리는 렌즈와 필름/센서 사이의 거리로 제어됩니다. 그래서 초점을 변경할 때 렌즈가 카메라에 대해 움직이는 것을 볼 수 있습니다(스마트폰 카메라에서도 이런 일이 발생할 수 있지만, 센서가 움직입니다). "조리개(aperture)"는 렌즈의 유효 크기를 제어하는 구멍입니다. 실제 카메라의 경우, 더 많은 빛이 필요하면 조리개를 크게 만들고, 초점 거리에서 멀어진 물체에 대해 더 많은 흐림을 얻게 됩니다. 우리의 가상 카메라는 완벽한 센서를 가질 수 있고 더 많은 빛이 필요하지 않으므로, 초점 흐림을 원할 때만 조리개를 사용합니다.
A Thin Lens Approximation
실제 카메라는 복잡한 복합 렌즈를 가지고 있습니다. 우리 코드에서는 센서, 렌즈, 조리개 순서를 시뮬레이션할 수 있습니다. 그런 다음 광선을 어디로 보낼지 파악하고, 계산 후 이미지를 뒤집을 수 있습니다(이미지가 필름에 거꾸로 투영되기 때문입니다). 하지만 그래픽스 전문가들은 일반적으로 얇은 렌즈 근사(thin lens approximation)를 사용합니다:
*그림 21: 카메라 렌즈 모델
카메라 내부를 시뮬레이션할 필요가 없습니다. 카메라 외부의 이미지를 렌더링하는 목적에서는 불필요한 복잡성이 될 것입니다. 대신, 무한히 얇은 원형 "렌즈"에서 광선을 시작하여, 모든 것이 완벽하게 초점이 맞는 초점 평면(렌즈에서 focalLength 떨어진 곳)에 있는 관심 픽셀을 향해 보냅니다.
실제로는 뷰포트를 이 평면에 배치하여 이를 구현합니다. 모든 것을 종합하면:
1.
초점 평면은 카메라 시야 방향에 직교합니다.
2.
초점 거리는 카메라 중심과 초점 평면 사이의 거리입니다.
3.
뷰포트는 초점 평면 위에 있으며, 카메라 시야 방향 벡터를 중심으로 합니다.
4.
픽셀 위치의 그리드는 뷰포트 내부에 있습니다(3D 세계에 위치).
5.
무작위 이미지 샘플 위치는 현재 픽셀 위치 주변 영역에서 선택됩니다.
6.
카메라는 렌즈의 무작위 지점에서 현재 이미지 샘플 위치를 통과하는 광선을 발사합니다.
*그림 22: 카메라 초점 평면
Generating Sample Rays
디포커스 블러가 없으면, 모든 장면 광선은 카메라 중심(또는 lookfrom)에서 시작됩니다. 디포커스 블러를 구현하기 위해, 카메라 중심을 중심으로 하는 디스크를 만듭니다. 반지름이 클수록 디포커스 블러가 더 커집니다. 원래 카메라는 반지름이 0인 디포커스 디스크를 가진 것으로 생각할 수 있습니다(블러가 전혀 없음). 따라서 모든 광선이 디스크 중심(lookfrom)에서 시작되었습니다.
그렇다면 디포커스 디스크는 얼마나 커야 할까요? 이 디스크의 크기가 디포커스 블러의 정도를 제어하므로, 카메라 클래스의 매개변수가 되어야 합니다. 디스크의 반지름을 카메라 매개변수로 사용할 수도 있지만, 블러가 투영 거리에 따라 달라집니다. 조금 더 쉬운 매개변수는 뷰포트 중심을 꼭짓점으로 하고 카메라 중심의 디포커스 디스크를 밑면으로 하는 원뿔의 각도를 지정하는 것입니다. 이렇게 하면 특정 샷에 대해 초점 거리를 변경할 때 더 일관된 결과를 얻을 수 있습니다.
디포커스 디스크에서 무작위 점을 선택할 것이므로, 이를 위한 함수가 필요합니다: random_in_unit_disk(). 이 함수는 random_unit_vector()에서 사용하는 것과 동일한 방식을 사용하지만, 2차원에 대해서만 작동합니다.
*Listing 85: [vec3.h] 단위 디스크 내부의 무작위 점 생성*
이제 카메라가 디포커스 디스크에서 광선을 시작하도록 업데이트해봅시다:
#pragma once
#ifndef CAMERA_H
#define CAMERA_H
#include "Hittable.h"
#include "Material.h"
class Camera
{
public:
double aspectRatio = 1.0; // Ratio of image width over height
int imageWidth = 100; // Rendered image width in pixel count
int samplesPerPixel = 10; // Count of random samples for each pixel
int maxDepth = 10; // Maximum number of ray bounces into scene
double vfov = 90; // 수직 시야각(시야)
Point3 lookfrom = Point3(0, 0, 0); // 카메라가 바라보는 위치
Point3 lookat = Point3(0, 0, -1); // 카메라가 바라보는 점
Vec3 vup = Vec3(0, 1, 0); // 카메라 상대 "위쪽" 방향
double defocus_angle = 0; // Variation angle of rays through each pixel
double focus_dist = 10; // Distance from camera lookfrom point to plane of perfect focus
void Render(const Hittable& world)
{
Initialize();
std::cout << "P3\n" << imageWidth << ' ' << mImageHeight << "\n255\n";
for (int scanlineIndex = 0; scanlineIndex < mImageHeight; scanlineIndex++)
{
std::clog
<< "\rScanlines remaining: "
<< (mImageHeight - scanlineIndex)
<< ' '
<< std::flush;
for (int pixelIndex = 0; pixelIndex < imageWidth; pixelIndex++)
{
Color pixelColor(0.0, 0.0, 0.0);
for (int sampleIndex = 0; sampleIndex < samplesPerPixel; sampleIndex++)
{
Ray ray = GetRay(pixelIndex, scanlineIndex);
pixelColor += RayColor(ray, maxDepth, world);
}
WriteColor(std::cout, mPixelSamplesScale * pixelColor);
}
}
std::clog << "\rDone. \n";
}
private:
void Initialize()
{
mImageHeight = static_cast<int>(imageWidth / aspectRatio);
mImageHeight = (mImageHeight < 1) ? 1 : mImageHeight;
mPixelSamplesScale = 1.0 / static_cast<double>(samplesPerPixel);
mCenter = lookfrom;
// Determine viewport dimensions
auto focalLength = (lookfrom - lookat).Length();
auto theta = DegreesToRadians(vfov);
auto h = std::tan(theta / 2);
auto viewportHeight = 2.0 * h * focus_dist;
auto viewportWidth = viewportHeight * (static_cast<double>(imageWidth) / mImageHeight);
// 카메라 좌표 프레임에 대한 u,v,w 단위 기저 벡터 계산
w = UnitVector(lookfrom - lookat);
u = UnitVector(Cross(vup, w));
v = Cross(w, u);
// 뷰포트의 수평 및 수직 가장자리를 가로지르는 벡터 계산
Vec3 viewportU = viewportWidth * u; // 뷰포트 수평 가장자리를 가로지르는 벡터
Vec3 viewportV = viewportHeight * -v; // 뷰포트 수직 가장자리를 따라 내려가는 벡터
// Calculate the horizontal and vertical delta vectors from pixel to pixel
mPixelDeltaU = viewportU / imageWidth;
mPixelDeltaV = viewportV / mImageHeight;
// Calculate the location of the upper left pixel
const Point3 viewportUpperLeft =
mCenter
- (focus_dist * w)
- viewportU / 2.0
- viewportV / 2.0;
mPixel00Location = viewportUpperLeft + 0.5 * (mPixelDeltaU + mPixelDeltaV);
const double defocusRadius =
focus_dist * std::tan(DegreesToRadians(defocus_angle * 0.5));
mDefocusDiskU = u * defocusRadius;
mDefocusDiskV = v * defocusRadius;
}
Ray GetRay(int pixelIndex, int scanlineIndex) const
{
// Construct a camera ray originating from the origin and directed at randomly sampled
// point around the pixel location pixelIndex, scanlineIndex.
auto offset = SampleSquare();
auto pixelSample =
mPixel00Location
+ ((pixelIndex + offset.X()) * mPixelDeltaU)
+ ((scanlineIndex + offset.Y()) * mPixelDeltaV);
auto rayOrigin = (defocus_angle <= 0.0) ? mCenter : DefocusDiskSample();
auto rayDirection = pixelSample - rayOrigin;
return Ray(rayOrigin, rayDirection);
}
Vec3 SampleSquare() const
{
// Returns the vector to a random point in the [-.5, -.5] - [+.5, +.5] unit square
return Vec3(RandomDouble() - 0.5, RandomDouble() - 0.5, 0.0);
}
Point3 DefocusDiskSample() const
{
const Vec3 point = RandomInUnitDisk();
return mCenter + (point.X() * mDefocusDiskU) + (point.Y() * mDefocusDiskV);
}
Color RayColor(const Ray& ray, int depth, const Hittable& world) const
{
// If we've exceeded the ray bounce limit, no more light is gathered
if (depth <= 0)
{
return Color(0.0, 0.0, 0.0);
}
HitRecord hitRecord;
if (world.Hit(ray, Interval(0.001, Infinity), hitRecord))
{
Ray scattered;
Color attenuation;
if (hitRecord.Material->Scatter(ray, hitRecord, attenuation, scattered))
{
return attenuation * RayColor(scattered, depth - 1, world);
}
return Color(0.0, 0.0, 0.0);
}
Vec3 unitDirection = UnitVector(ray.Direction());
auto a = 0.5 * (unitDirection.Y() + 1.0);
return (1.0 - a) * Color(1.0, 1.0, 1.0)
+ a * Color(0.5, 0.7, 1.0);
}
private:
int mImageHeight = 0; // Rendered image height
Point3 mCenter; // Camera center
Point3 mPixel00Location; // Location of pixel 0, 0
Vec3 mPixelDeltaU; // Offset to pixel to the right
Vec3 mPixelDeltaV; // Offset to pixel below
Vec3 u, v, w; // 카메라 프레임 기저 벡터
double mPixelSamplesScale = 1.0; // Color scale factor for a sum of pixel samples
Vec3 mDefocusDiskU; // Defocus disk horizontal radius
Vec3 mDefocusDiskV; // Defocus disk vertical radius
};
#endif
C++
복사
*Listing 86: [camera.h] 조절 가능한 피사계 심도를 가진 카메라*
큰 조리개를 사용하는 경우:
int main()
{
// ...
Camera camera;
camera.aspectRatio = 16.0 / 9.0;
camera.imageWidth = 400;
camera.samplesPerPixel = 100;
camera.maxDepth = 50;
camera.verticalFieldOfView = 20.0;
camera.lookFrom = Point3(-2.0, 2.0, 1.0);
camera.lookAt = Point3(0.0, 0.0, -1.0);
camera.viewUp = Vec3(0.0, 1.0, 0.0);
camera.defocusAngle = 10.0;
camera.focusDistance = 3.4;
camera.Render(world);
}
C++
복사
결과:
*Image 22: 피사계 심도를 가진 구체들*





