모션 블러(Motion Blur)
레이 트레이싱을 하기로 결정했다는 것은, 실행 시간보다 시각적 품질을 더 중요하게 보겠다는 뜻입니다. 흐릿한 반사(fuzzy reflection)와 디포커스 블러(defocus blur)를 렌더링할 때는 픽셀당 여러 샘플을 사용했습니다. 한 번 그 길로 들어서면 좋은 소식은, 거의 모든 효과를 비슷한 방식으로(브루트포스) 구현할 수 있다는 점입니다. 모션 블러는 그 대표적인 예입니다.
실제 카메라에서는 셔터가 짧은 시간 동안 열려 있고, 그 동안 카메라와 세계 안의 물체가 움직일 수 있습니다. 이런 촬영 결과를 정확히 재현하려면, 셔터가 열려 있는 동안 카메라가 “감지한 것”의 시간 평균을 구해야 합니다.
시공간(Spacetime) 레이 트레이싱 소개
셔터가 열려 있는 동안 임의의 시각 하나를 뽑아, 그 시각에 레이 하나를 쏘면 단일(단순화된) 광자에 대한 무작위 추정을 얻을 수 있습니다. 그 시각에 물체들이 어디에 있어야 하는지만 알 수 있다면, 해당 레이가 같은 시각에서 측정한 빛의 양을 정확히 평가할 수 있습니다. 이는 무작위(몬테카를로) 레이 트레이싱이 얼마나 단순해질 수 있는지 보여주는 또 하나의 예입니다. 이번에도 브루트포스가 승리합니다.
레이 트레이서의 “엔진”은 각 레이에 대해 물체가 있어야 할 위치에 있도록만 보장하면 되므로, 교차(intersection) 계산의 핵심은 크게 바뀌지 않습니다. 이를 위해서는 각 레이마다 정확한 시간을 저장해야 합니다.
Ray.h — time 필드 추가
// 변경: 생성자에 time 파라미터 추가
__device__ Ray(const Point3& origin, const Vector3& direction, double time = 0.0)
: mOrig(origin), mDir(direction), mTime(time) {}
// 변경: 복사 생성자에 mTime 추가
__device__ Ray(const Ray& other)
: mOrig(other.mOrig), mDir(other.mDir), mTime(other.mTime) {}
// 추가: time 접근자
__device__ double Time() const { return mTime; }
// 추가: private 멤버
double mTime; // 레이가 발사된 시각
---
C++
복사
시간 관리(Managing Time)
계속 진행하기 전에, 시간을 어떻게 다룰지 생각해 봅시다. 셔터 타이밍에는 크게 두 가지 측면이 있습니다.
•
한 번 셔터가 열리고 다음 번 셔터가 열릴 때까지의 간격(프레임 간 주기)
•
각 프레임에서 셔터가 실제로 열려 있는 시간(노출 시간)
표준 영화 필름은 초당 24프레임으로 촬영되곤 했습니다. 현대 디지털 영상은 24, 30, 48, 60, 120 등 감독이 원하는 다양한 프레임 레이트를 사용할 수 있습니다.
각 프레임은 서로 다른 셔터 속도를 가질 수 있습니다. 그리고 이 셔터 속도는 프레임 전체 길이의 최대치일 필요가 없고, 실제로도 대개 그렇지 않습니다. 예를 들어 매 프레임마다 1/1000초만 셔터를 열 수도 있고, 1/60초 동안 열 수도 있습니다.
만약 이미지 시퀀스를 렌더링하려면, 카메라에 다음 정보를 적절히 설정해야 합니다.
•
프레임 간 주기
•
셔터(렌더) 지속 시간
•
전체 프레임 수(전체 샷 시간)
카메라가 움직이고 월드가 정적이면 이 설정만으로 충분합니다. 하지만 월드 안의 어떤 물체라도 움직인다면, 각 오브젝트가 현재 프레임의 시간 구간을 알 수 있도록 hittable에 메서드를 추가해야 합니다. 그러면 모든 애니메이션 오브젝트가 해당 프레임 동안의 움직임을 설정할 수 있습니다.
이 방식은 비교적 직관적이고, 직접 실험해 보면 재미있는 주제입니다. 다만 여기서는 더 단순한 모델로 진행하겠습니다.
•
단 하나의 프레임만 렌더링합니다.
•
시간은 t=0에서 시작해 t=1에서 끝난다고 암묵적으로 가정합니다.
따라서 해야 할 일은 두 가지입니다.
1.
카메라가 [0,1]에서 임의의 시간을 뽑아 그 시각의 레이를 쏘도록 수정하기
2.
애니메이션(이동) 구를 구현하기
모션 블러를 위한 카메라 업데이트
카메라가 시작 시간과 종료 시간 사이의 임의의 시각에서 레이를 생성하도록 바꿔야 합니다. 카메라가 시간 구간을 관리할지, 아니면 레이를 만들 때 카메라 사용자 쪽에서 시간을 주입할지 선택해야 합니다. 개인적으로는 생성자를 복잡하게 하더라도 호출부를 단순하게 만드는 것을 선호하기 때문에, 여기서는 카메라가 시간 구간을 들고 있도록 하겠습니다.
현재는 카메라 자체가 움직이지 않는다고 가정하므로, 카메라가 하는 일은 일정 시간 구간에 걸쳐 레이를 발사하는 것뿐입니다.
Camera.h — 셔터 시간 추가
// 변경: 생성자에 time0, time1 파라미터 추가
__device__ Camera(
Point3 lookfrom, Point3 lookat, Vector3 vup,
double vfov, double aspect, double aperture, double focusDist,
double time0 = 0.0, // 추가
double time1 = 0.0) // 추가
{
mTime0 = time0; // 추가
mTime1 = time1; // 추가
// ... 나머지 기존 코드 동일
}
// 변경: GetRay()에서 랜덤 time 생성 후 Ray에 전달
__device__ Ray GetRay(double s, double t, curandState* randState) const
{
Vector3 rd = mLensRadius * RandomInUnitDisk(randState);
Vector3 offset = mU * rd.X() + mV * rd.Y();
double time = mTime0 + curand_uniform(randState) * (mTime1 - mTime0); // 추가
return Ray(
mOrigin + offset,
mLowerLeftCorner + s * mHorizontal + t * mVertical - mOrigin - offset,
time); // 추가
}
// 추가: private 멤버
double mTime0;
double mTime1;
C++
복사
이동하는 구(Moving Spheres) 추가
이제 움직이는 오브젝트를 만들어 봅시다. 구의 중심이 시간 t=0일 때 center1에서 시작해, 시간 t=1일 때 center2까지 선형으로 이동하도록 구 클래스를 업데이트하겠습니다. (t=0~1 범위 밖에서도 선형으로 계속 연장되므로, 어떤 시간에서든 샘플링할 수 있습니다.)
이를 위해 3D 중심점 하나 대신,
•
t=0에서의 시작 위치
•
t=1까지의 변위(displacement)
를 담는 3D 레이로 중심을 표현하겠습니다.
MovingSphere.h — 신규 파일
#pragma once
#ifndef MOVING_SPHERE_H
#define MOVING_SPHERE_H
#include "Hittable.h"
class MovingSphere : public Hittable
{
public:
__device__ MovingSphere() {}
__device__ MovingSphere(
Point3 center0, Point3 center1,
double time0, double time1,
double radius, Material* material)
: mCenter0(center0), mCenter1(center1)
, mTime0(time0), mTime1(time1)
, mRadius(radius), mMaterial(material) {}
__device__ Point3 Center(double time) const
{
return mCenter0 + ((time - mTime0) / (mTime1 - mTime0)) * (mCenter1 - mCenter0);
}
__device__ bool Hit(
const Ray& ray, double tMin, double tMax, HitRecord& hitRecord) const override
{
// 레이 시각에 따라 중심 위치 보간
double frac = (ray.Time() - mTime0) / (mTime1 - mTime0);
Point3 currentCenter = mCenter0 + frac * (mCenter1 - mCenter0);
Vector3 oc = ray.Origin() - currentCenter;
double a = Dot(ray.Direction(), ray.Direction());
double b = Dot(oc, ray.Direction());
double c = Dot(oc, oc) - mRadius * mRadius;
double discriminant = b * b - a * c;
if (discriminant > 0.0)
{
double temp = (-b - sqrt(discriminant)) / a;
if (temp < tMax && temp > tMin)
{
hitRecord.T = temp;
hitRecord.P = ray.At(hitRecord.T);
Vector3 outwardNormal = (hitRecord.P - currentCenter) / mRadius;
hitRecord.SetFaceNormal(ray, outwardNormal);
hitRecord.MaterialPtr = mMaterial;
return true;
}
temp = (-b + sqrt(discriminant)) / a;
if (temp < tMax && temp > tMin)
{
hitRecord.T = temp;
hitRecord.P = ray.At(hitRecord.T);
Vector3 outwardNormal = (hitRecord.P - currentCenter) / mRadius;
hitRecord.SetFaceNormal(ray, outwardNormal);
hitRecord.MaterialPtr = mMaterial;
return true;
}
}
return false;
}
private:
Point3 mCenter0, mCenter1;
double mTime0, mTime1;
double mRadius;
Material* mMaterial;
};
#endif
C++
복사
업데이트된 sphere::hit() 함수는 기존과 거의 동일합니다. 달라진 점은 애니메이션 중심의 현재 위치를 레이의 시간 r.time()으로 계산한다는 것뿐입니다.
// Listing 4: sphere.h - 이동하는 구의 hit 함수(핵심 부분)
class sphere : public hittable {
public:
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
point3 current_center = center.at(r.time());
vec3 oc = current_center - r.origin();
auto a = r.direction().length_squared();
auto h = dot(r.direction(), oc);
auto c = oc.length_squared() - radius * radius;
...
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - current_center) / radius;
rec.set_face_normal(r, outward_normal);
get_sphere_uv(outward_normal, rec.u, rec.v);
rec.mat = mat;
return true;
}
...
};
C++
복사
레이 교차 시간 전달(재질 Scatter 업데이트)
레이에 시간 속성이 생겼으므로, material::scatter() 메서드들도 교차 시각을 고려하도록 수정해야 합니다. 핵심은 새로 생성하는 scattered 레이가 입력 레이 r_in의 시간을 그대로 물려받도록 하는 것입니다.
Material.h — Lambertian Scatter 수정
// 변경: scatter direction 계산 방식 + NearZero 처리 + time 전달
__device__ bool Scatter(...) const override
{
Vector3 scatterDirection = rec.Normal + RandomInUnitSphere(randState);
// 추가: 산란 방향 퇴화 방지
if (scatterDirection.NearZero())
scatterDirection = rec.Normal;
// 변경: rayIn.Time() 전달
scattered = Ray(rec.P, scatterDirection, rayIn.Time());
attenuation = mAlbedo;
return true;
}
---
Metal.h — Scatter 수정
// 변경: rayIn.Time() 전달
scattered = Ray(rec.P, reflected + mFuzz * RandomInUnitSphere(randState), rayIn.Time());
---
Dielectric.h — Scatter 수정
// 변경: rayIn.Time() 전달
scattered = Ray(rec.P, direction, rayIn.Time());
C++
복사
전체 코드 합치기(Putting Everything Together)
아래 코드는 이전 장 마지막에 사용했던 예제 장면(난반사 구들)을 그대로 가져오되, 렌더링 중에 구들이 움직이도록 만든 것입니다. 각 구는 시간 t=0에서 중심 C에 있다가, 시간 t=1에서 C+(0, r/2, 0)로 이동합니다.
kernel.cu — CreateWorld 수정
// 추가: include
#include "MovingSphere.h"
// 변경: CreateWorld 시그니처에 outCount 추가
__global__ void CreateWorld(..., int* outCount)
// 변경: Lambertian 소형 구체 → MovingSphere로 교체
if (chooseMat < 0.8)
{
Vector3 center2 = center + Vector3(0.0, 0.5 * RND, 0.0); // 추가: 이동 목표점
list[i++] = new MovingSphere(
center, center2, 0.0, 1.0, 0.2,
new Lambertian(Color(RND * RND, RND * RND, RND * RND)));
}
// 추가: 대형 구체 근처 겹침 방지
Vector3 diff = center - Vector3(4.0, 0.2, 0.0);
if (diff.Length() <= 0.9) continue;
// 변경: 실제 배치된 수로 HittableList 생성
*world = new HittableList(list, i);
*outCount = i;
// 변경: 카메라에 셔터 시간 추가
*camera = new Camera(lookfrom, lookat, ..., 0.0, 1.0); // time0=0, time1=1
// 추가: main()에서 실제 count 추적
int* d_numHittables;
cudaMallocManaged((void**)&d_numHittables, sizeof(int));
CreateWorld<<<1,1>>>(..., d_numHittables);
int numHittables = *d_numHittables;
// 변경: 스택 크기
cudaDeviceSetLimit(cudaLimitStackSize, 32768);
C++
복사
결과는 아래와 같습니다.
*Image 1: 튀는 구들(Bouncing spheres)*



