CPP로 RayTracing구현- 1. Ray를 이용해 색상 띄우기

그래픽스꿀잼·2026년 4월 28일

그래픽스

목록 보기
9/20
post-thumbnail

[본 글은 RayTracing_in_one_weekend를 공부하며 작성하는 정리글입니다]

PPM 이미지 사용하기

ppm은 텍스트로 구성된 이미지 포맷임

0 0 0은 검은색, 255 255 255는 흰색 이런식으로 ㅇㅇ

#include <iostream>

int main() {
    // Image
    const int image_width = 256;
    const int image_height = 256;

    // Render
    std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";

    for (int j = 0; j < image_height; j++) {
        for (int i = 0; i < image_width; i++) {
            auto r = double(i) / (image_width - 1);
            auto g = double(j) / (image_height - 1);
            auto b = 0.0;

            int ir = int(255.999 * r);
            int ig = int(255.999 * g);
            int ib = int(255.999 * b);

            std::cout << ir << ' ' << ig << ' ' << ib << '\n';
        }
    }
}

대충 이런 코드를 작성해보자

for문이 먼저 height인 j, width인 i 순서로 되어있는거 유의!

image height과 width는 256으로 맞춰져 있음

i와 j를 이용해 r과 g를 0부터 255까지 변화시킴

이제 이것을 cmake를 이용해

cmake -B build
cmake --build build
build\Debug\inOneWeekend.exe > image.ppm

cmakeLists.text에 저장된 응용프로그램이름으로 image.ppm이름을 가진 파일을 만들음

이는

image.ppm

이런 사진으로 변환됨

Vector3, Color, Point3, Ray 클래스 만들기

먼저 Vector3클래스, Color클래스, Point3클래스 라는 클래스들을 만들어줄거임

이 중 Color, Point3클래스는 Vector3의 의미를 바꾼 설탕클래스에 불과함

Vector3는 x,y,z를 가진 3차원 벡터를 나타냄

Color는 r,g,b를 가진 색상을 나타냄

Point3는 x,y,z를 가진 3차원 공간의 정점을 나타냄

Vector3, Point3, Color 클래스

#ifndef VECTOR3_H
#define VECTOR3_H

#include <cmath>
#include <iostream>

class Vector3
{
public:
    double e[3];

    Vector3(): e{0, 0, 0}{ }

    Vector3(double e0, double e1, double e2) : e{e0, e1, e2}{ }

    double x() const { return e[0]; }
    double y() const { return e[1]; }
    double z() const { return e[2]; }

    //자기를 반환하는 연산자 오버로딩
    Vector3 operator-() const { return Vector3(-e[0], -e[1], -e[2]); }
    double operator[](int i) const { return e[i]; }
    double& operator[](int i) { return e[i]; }

    Vector3& operator*=(double t)
    {
        e[0] *= t;
        e[1] *= t;
        e[2] *= t;
        return *this;
    }

    Vector3& operator/=(double t)
    {
        return *this *= 1 / t;
    }

    //벡터 길이 (제곱근) : 루트없는 버전의 제곱근
    double length() const
    {
        return std::sqrt(length_squared());
    }

    //벡터 길이(제곱근 없이) : 모든 벡터 길이의 제곱
    double length_squared() const
    {
        return e[0] * e[0] + e[1] * e[1] + e[2] * e[2];
    }
};

//점과 벡터의 분리를 위해 사용하는 문법설탕
using Point3 = Vector3;

//새로운 벡터3 반환하는 연산자 오버로딩
inline std::ostream& operator<<(std::ostream& out, const Vector3& v)
{
    return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2];
}

inline Vector3 operator+(const Vector3& u, const Vector3& v)
{
    return Vector3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]);
}

inline Vector3 operator-(const Vector3& u, const Vector3& v)
{
    return Vector3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]);
}

inline Vector3 operator*(const Vector3& u, const Vector3& v)
{
    return Vector3(u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2]);
}

inline Vector3 operator*(double t, const Vector3& v)
{
    return Vector3(t * v.e[0], t * v.e[1], t * v.e[2]);
}

inline Vector3 operator*(const Vector3& v, double t)
{
    return t * v;
}

inline Vector3 operator/(const Vector3& v, double t)
{
    return (1 / t) * v;
}

//내적 공식 : ux*vx + uy*vy + uz*vz
inline double dot(const Vector3& u, const Vector3& v)
{
    return u.e[0] * v.e[0]
        + u.e[1] * v.e[1]
        + u.e[2] * v.e[2];
}

//외적 공식 : uy*vz - uz*vy, uz*vx - ux*vz, ux*vy - uy*vx
inline Vector3 cross(const Vector3& u, const Vector3& v)
{
    return Vector3(u.e[1] * v.e[2] - u.e[2] * v.e[1],
                u.e[2] * v.e[0] - u.e[0] * v.e[2],
                u.e[0] * v.e[1] - u.e[1] * v.e[0]);
}

//단위벡터 공식 : 벡터 / 벡터의 길이
inline Vector3 unit_vector(const Vector3& v)
{
    return v / v.length();
}

#endif

//--------------------------------------------------------------------

#ifndef COLOR_H
#define COLOR_H

#include "Vector3.h"
#include <iostream>

using Color = Vector3;

void write_color(std::ostream& out, const Color& pixel_color) {
    double r = pixel_color.x();
    double g = pixel_color.y();
    double b = pixel_color.z();

    // Translate the [0, 1] component values to the byte range [0, 255].
    int rbyte = static_cast<int>(255.999 * r);
    int gbyte = static_cast<int>(255.999 * g);
    int bbyte = static_cast<int>(255.999 * b);

    // Write out the pixel color components.
    out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n';
}

#endif

Ray클래스

AA에서 bb방향으로 tt시간만큼 이동한 위치가 Ray임

A+tb=PA + tb = P(도착위치) 임

t는 실수임.
음수 t값이 되면 반대 방향 벡터로도 이동가능함
양수 t값을 이용해 b방향으로 이동한거를 반직선, Ray라고 부름

#ifndef RAY_H
#define RAY_H

#include "Vector3.h"

class Ray {
public:
    Ray() {}

    Ray(const Point3& origin, const Vector3& direction) : orig(origin), dir(direction) {}

    const Point3& origin() const { return orig; }
    const Vector3& direction() const { return dir; }

    Point3 at(double t) const {
        return orig + t * dir;
    }

private:
    Point3 orig;
    Vector3 dir;
};

#endif

at메서드가 Ray공식임dd

색 블렌딩

color ray_color(const ray& r) {
  return color(0, 0, 0);
}

먼저 이런 메서드를 main에 만들어줌

나중에 따로 구현할거 ㅇㅇ

int main()
{
    auto aspect_ratio = 16.0 / 9.0;
    int image_width = 400;


    //height  = width / 16 * 9 = width * 9 / 16 = width / (16 / 9)
    int image_height = int(image_width / aspect_ratio);
    //height가 1보다 작으면 최소한 1이라도 보장하기
    image_height = (image_height < 1) ? 1 : image_height;

    // Camera
    //카메라부터 뷰표트까지의 거리
    double focal_length = 1.0;
    double viewport_height = 2.0;
    //해상도 비율을 사용하지 않고, 이미지를 사용하는 이유
    //해상도 이미지는 이상적인 비율이고, 실제 이미지의 비율은 해상도 비율과 다를 수 있음
    //width = height / 9 * 16 = height * 16 / 9
    double viewport_width = viewport_height * (double(image_width) / image_height);
    Point3 camera_center = Point3(0, 0, 0);

    //뷰표트의 왼->오, 상->하로 이동하는 최대 좌표 구하기
    Vector3 viewport_u = Vector3(viewport_width, 0, 0);
    Vector3 viewport_v = Vector3(0, -viewport_height, 0);

    //뷰포트의 각 픽셀간의 거리 구하기(델타 벡터)
    Vector3 pixel_delta_u = viewport_u / image_width;
    Vector3 pixel_delta_v = viewport_v / image_height;

    //최 좌상단 픽셀의 좌표구하기
    //camera_center - Vector3(0, 0, focal_length) : 뷰포트 바로 위에서 focal_length만큼 z방향 반대로 떨어지기
    // - viewport_u/2 - viewport_v/2 : 화면의 가로세로 절반씩 좌, 상으로 이동하기(뺄셈)
    Vector3 viewport_upper_left = camera_center - Vector3(0, 0, focal_length) - viewport_u / 2 - viewport_v / 2;
    //위의 공식은 뷰포트의 최좌상단 좌표가 됨. 하지만 픽셀 시작 좌표는 아님
    //따라서 시작지점은 뷰포트 픽셀거리의 절반만큼 띄워진 거리에서부터 시작해야 정확히 너비 * 높이 개의 영역으로 균등하게 나눠짐
    Vector3 pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);

    //render
    cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";

    for (int j = 0; j < image_height; ++j)
    {
        clog << "\rScanLines remaining: " << (image_height - j) << ' ' << flush;
        for (int i = 0; i < image_width; ++i)
        {
            Vector3 pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v);
            Vector3 ray_dir = pixel_center - camera_center;
            Ray r = Ray(camera_center, ray_dir);

            Color pixel_color = ray_color(r);

            write_color(std::cout, pixel_color);
        }
    }

    clog << "\rDone.                                                     \n";
}

메인 메서드는 위처럼 바꿔줌

메인 아이디어는 다음과 같음

  1. 해상도 비율이 있음
    해상도 비율과 height, width를 이용해 width, height를 구함 (식 유도는 위 코드 주석 참조)
  2. camera를 만들어줌
    카메라는 뷰포트를 바라보는 하나의 좌표임(오른손 좌표계를 이용함)
    focal_length는 뷰포트에서 카메라까지 z축의 거리임
  3. 이미지는 좌상단 -> 우하단으로 렌더링됨.
    따라서 뷰포트 너비를 u,v를 좌->우, 상->하로 계산함
  4. 뷰포트 너비를 이용해 뷰포트 픽셀당 거리를 계산함
  5. camera_center의 좌표와 focal_length, u,v를 이용해 화면 좌상단 좌표를 구함
    근데 좌상단 좌표에서부터 픽셀이 시작되면 각 픽셀의 너비*높이가 일정해지지 않음
    따라서 픽셀간의 거리를 델타u, 델타v라고 하면 델타u, 델타v의 절반만큼씩 좌상단에서 이동된 거리에서부터 픽셀이 시작되도록 함
    이러면 뷰포트의 픽셀간 거리가 너비 * 높이로 일정해짐
  6. 픽셀별로 좌표를 계산함
    Ray를 만드는데, ray는 카메라 좌표에서부터 픽셀좌표 - 카메라 좌표의 방향을 가지는 광선임.
    즉, Ray = A: camera_center, B: camera_center - pixel_center인거임
  7. 이 Ray를 이용해 뷰포트에 표시될 이미지 픽셀별로 색상을 계산때려버리면 됨
    색상 계산로직을 이제 수정해보자
Color ray_color(const Ray& r)
{
    Vector3 unit_dir = unit_vector(r.direction());

    //unit_dir은 정규화된 벡터로 -1~1사이의 값을 가짐
    //하지만 색상에 -1~0사이의 값은 없음
    //따라서 이를 0~1로 정규화해줘야함
    //그게 (unit_dir + 1) / 0.5인거임
    //-1일때 : (-1 + 1) / 0.5 = 0
    //1일때 : (1 + 1) / 0.5 = 1
    //즉, -1일때는 최하단으로 흰색
    //0일때는 중간으로 (1,1,1)과 (0.5,0.7,1.0)의 중간 혼합색(선형 블렌딩)
    //1일때는 최상단으로 (0.5,0.7,1.0)색
    double a = 0.5 * (unit_dir.y() + 1.0);

    return (1.0 - a) * Color(1.0, 1.0, 1.0) + a * Color(0.5, 0.7, 1.0);
}

ray_color메서드를 위처럼 바꿈

ray의 방향을 정규화된 벡터로 바꿔줌.

Vector3.h에서 구현한 정규화 식은 다음과 같음

unit_vector메서드

  • ray_color메서드
    ray의 방향을 정규화해주고, 정규화된 벡터의 y방향(2차원에서 세로방향)을 다시 0~1사이의 값으로 정규화 해줌

그리고 이 정규화된 값을 이용해

최하단일때는 1.0, 1.0, 1.0인 색상(흰색)으로 표현하고,

최상단일때는 0.5, 0.7, 1.0인 색상(하늘색)으로 표현하고,

그 외의 구역은 선형 블렌딩 공식인 (1 - a) ColorA + a ColorB를 이용해 색상혼합을 해주면 됨

그럼 위와같이 예쁘게 블렌딩된 색상이 나옴

이 ray_color코드를 수정하면 원하는 색상을 마음대로 뽑을 수 있음

이런 혼합색상도 만들 수 있음

혹시 몰라서 위 색상을 원하는 사람들을 위해 ray_color를 올려줌

Color ray_color(const Ray& r)
{
    Vector3 unit_dir = unit_vector(r.direction());

    double x = unit_dir.x();
    double y = unit_dir.y();

    // 1. 중심으로부터의 거리 (반지름)를 계산
    double radius = std::sqrt(x * x + y * y);

    // 중앙 색상 (흰색)
    Color center_color(1.0, 1.0, 1.0);

    // 거리가 0에 매우 가깝다면 바로 흰색을 반환
    if (radius < 1e-8) {
        return center_color;
    }

    // 2. 상하좌우 지정된 색상 정의
    Color left_color(0.5, 0.7, 1.0);   // 좌 : 하늘색
    Color right_color(1.0, 0.7, 0.8);  // 우 : 연분홍색
    Color bottom_color(0.5, 1.0, 0.5); // 하 : 연두색
    Color top_color(1.0, 0.6, 0.2);    // 상 : 주황색

    // 3. 현재 광선의 방향에 따라 목표로 하는 x축, y축 색상을 결정
    Color x_axis_color = (x < 0) ? left_color : right_color;
    Color y_axis_color = (y < 0) ? bottom_color : top_color;

    // 4. x와 y의 절댓값 비율을 계산하여 모서리 부분의 색상을 자연스럽게 혼합
    double abs_x = std::abs(x);
    double abs_y = std::abs(y);
    double weight_sum = abs_x + abs_y;

    Color edge_color = (abs_x / weight_sum) * x_axis_color + 
                       (abs_y / weight_sum) * y_axis_color;

    // 5. 최종 블렌딩
    double blend_factor = std::min(radius, 1.0);
    
    return (1.0 - blend_factor) * center_color + blend_factor * edge_color;
}
profile
그래픽스 공부중

0개의 댓글