Rust实现Ray Tracing In One Weekend(1):基础结构篇

学了一段时间Rust和Games101的图形学入门课程,有点想做个软渲染器玩玩,正好看到了Ray Tracing In One Weekend的文章,觉得我应该能依葫芦画瓢复刻下,巩固各种知识,于是就有了这个项目。

仓库地址:https://github.com/YostGray/rust_soft_render.

最终效果

1.实现图片输出

原书中使用了PMM格式作为输出的图片,它没有多余的头,也没有调色板和其它的压缩算法,就是RGB值的序列化二进制文件,用来作为简单输出图像的格式还蛮好的。

但我就是爱折腾,于是实现了个小的BMP格式图像的输出库。

#[derive(Debug,Clone)]
pub struct Color{
    r : u8,
    g : u8,
    b : u8,
    a : u8,
}

#[derive(Clone)]
pub struct Img {
    width : u32,
    height : u32,
    pixels : Vec<Color>, //line first
}

它在数据层面上主要由Color和Img两个结构体构成。这里很好理解,颜色和颜色的集合组成了图片,在内存中操作图片时,用这种方式挺直观明了的。

impl Add for Color {
    type Output = Color;

    fn add(self, rhs: Color) -> Self::Output {
        Self::Output {
            r: self.r + rhs.r,
            g: self.g + rhs.g,
            b: self.b + rhs.b,
            a: self.a + rhs.a,
        }
    }
}

impl AddAssign for Color {
    fn add_assign(&mut self, rhs: Self) {
        self.r += rhs.r;
        self.g += rhs.g;
        self.b += rhs.b;
        self.a += rhs.a;
    }
}

接下来我在Color上实现了常用操作符的trait;以加法为例,分为+和+=实现,其中+会生成一个新的实例,而+=则在原来的实例上修改,在适当的时候调用适当的能节约一些寄存器拷贝和内存的开销,算是个小小的优化。

后面写了写简单的Color用函数,不在一一赘述。

#[cfg(test)]
mod test_color {
    use super::*;

    #[test]
    fn color_simple_test() {
        let mut black = Color::new(255, 255, 255, 255);
        let mut white = Color::new(0, 0, 0, 255);
        black.color_to_gray();
        white.color_to_gray();
        assert_eq!(black,Color::new(255, 255, 255, 255));
        assert_eq!(white,Color::new(0, 0, 0, 255));

        let mid_color_result = Color::linear_interpolation(&black,&white,0.5);
        assert!(mid_color_result.is_ok());
        let mid_color = mid_color_result.unwrap();
        assert!(mid_color.r >= 127 && mid_color.r <= 128);//float precision
    }
}

接下来则是写了个简单的测试,保证后续修改中,当前的逻辑没问题。

BMP图片小解

维基百科里解释的比较全面了,简单来说,BMP由一系列数据块构成,其中有Head,DIBHead,PixelArray是必须的,其它都是用于压缩或者色彩管理的可选项,暂时忽略。

pub struct BMPFile {
    header: BMPHeader,
    dib_header: DIBHeader,
    color_table: ColorTable,
    img: Img,
}
pub struct BMPHeader{
    /// usually use BM – Windows 3.1x, 95, NT, ... etc.
    bitmap_type: [char; 2],
    /// specifies the size of the file in bytes
    size: u32,
    reserved1: u16,
    reserved2: u16,
    /// Specifies the offset from the beginning of the file to the bitmap data.
    offset: u32,
}
///Common support version, without OS/2
pub struct DIBHeader{
    /// specifies the size of the BitMapFileHeader structure, in bytes, as now it's 40
    size: u32,
    /// specifies the width of the image, in pixels
    width: u32,
    /// specifies the height of the image, in pixels
    height: u32,
    /// specifies the number of planes of the target device, must be set to one
    planes: u16,
    /// specifies the number of bits per pixel
    /// possible values are as follows:
    ///  - 1 (black / white)
    ///  - 4 (16 colors)
    ///  - 8 (256 colors)
    ///  - 24 (16.7 million colors)
    bit_depth: u16,
    /// specifies the type of compression, usually set to zero (no compression)
    compression: u32,
    /// specifies the size of the image data, in bytes. If there is no
    /// compression, it is valid to set this member to zero
    size_image: u32,
    /// specifies the the horizontal pixels per meter on the designated target
    /// device, usually set to zero.
    x_pixels_per_meter: u32,
    /// specifies the vertical pixels per meter on the designated target device,
    /// usually set to zero
    y_pixels_per_meter: u32,
    /// specifies the number of colors used in the bitmap, if set to zero the
    /// number of colors is calculated using the biBitDepth member.
    colors_used: u32,
    /// specifies the number of color that are 'important' for the bitmap, if set
    /// to zero, all colors are important
    colors_important: u32,
}

主要的数据结构如代码所示,这里的主要工作就是把Img结构存储为.bmp文件,具体可以查看GitHub仓库。

接下来输出了一张图片作为测试:

rg从左下到右上255-0变化

2.基础结构

首先第一个实现的是向量结构,Vector3,主要就是一些向量的各种运算。

#[derive(Debug, Clone, Copy)]
pub struct Vector3{
    x : f64,
    y : f64,
    z : f64,
}
//点成、叉乘、插值、归一化等等

然后是Ray,它由原点和方向构成。并有一个关键的get_color方法来确定返回到摄像机中的颜色。光线会不断反弹直到什么也没打中或者反弹depth次为止。

在什么也没打中的时候,会有个建议的蓝色天空盒用角度的方式定义出来作为背景。

蓝色背景
pub struct Ray{
    ori : Vector3,
    dir : Vector3,
}

impl Ray {
    pub fn new(ori: Vector3, dir: Vector3) -> Self { 
        Self { ori, dir:dir.normallized() } 
    }

    pub fn get_ori(&self) -> &Vector3 {
        &self.ori
    }

    pub fn get_dir(&self) -> &Vector3 {
        &self.dir
    }
    
    pub fn at(&self, t:f64) -> Vector3 {
        self.ori + self.dir * t
    }

    pub fn get_color(&self, s:&Scene, depth:u64) -> Vector3{
        if depth <= 0  {
            return Vector3::new(0.0, 0.0, 0.0);
        }
        match s.try_hit(self) {
            Option::Some(hr) => {
                let reflect_ray = Ray::new(hr.get_pos().clone(), hr.get_out_dir().clone());
                let eval_color = hr.get_eval_color().clone();
                let c = eval_color * (reflect_ray.get_color(s,depth - 1));
                return c;
            },
            Option::None => {
                let test = 0.5 * (self.dir.get_y() + 1.0);
                if depth == 49 {
                    s.try_hit(self);
                }
                Vector3::new(1.0, 1.0, 1.0) * (1.0 - test) + Vector3::new(127.0 / 255.0, 178.0 / 255.0, 255.0 / 255.0) * test
            },
        }
    }
}

geometry.rs中定义了一些几何体被射线击中后的结果。

HitResult 是击中的结果具体信息,包含击中位置,击中的发现,是从背面还是正面来,出入方向,材质,最终颜色等信息。

Hitable Trait,来标记可被击中的物体的部分。作为dyn traid来抽象不同的物体。

pub struct HitResult {
    pos : Vector3,
    normal : Vector3,
    t : f64,
    is_front_face :bool,

    in_dir : Vector3,
    out_dir : Vector3,

    mat : Box<Arc<dyn Mat + Send + Sync>>,
    eval_color : Vector3,
}

pub trait Hitable {
    fn try_hit(&self,ray:&Ray) -> Option<HitResult>;
}

Scene比较简单,就是个物体的集合。实现Sync和Send是因为要多线程渲染。

Hitable Trait则是让Ray只用考虑一种交互。

pub struct Scene {
    obj_list:Vec<Box<dyn Hitable + Sync + Send>>,
}

impl Hitable for Scene {
    fn try_hit(&self,ray:&super::ray::Ray) -> Option<HitResult> {
        let mut min_t = INFINITY;
        let mut result = Option::None;
        for o in &self.obj_list {
            match o.try_hit(ray) {
                Option::Some(r) => {
                    if min_t > r.get_t() {
                        min_t = r.get_t();
                        result = Option::Some(r);
                    }
                },
                Option::None => (),
            }
        }
        result
    }
}

Cam相机是个比较复杂的结构,主要确定了需要怎样渲染场景。

#[derive(Clone, Copy)]
pub struct Camera {
    screen_w : u32,//输出的宽
    screen_h : u32,//输出的高

    pos : Vector3,//相机所在位置
    dir : Vector3,//相机朝向
    dir_w : Vector3,
    dir_up : Vector3,//哪里是上方,和朝向可以叉乘出哪里是dir_w用来确定相机的形态

    near : f64,//近裁剪面
    fov_w  : f64,//degree FOV视场角
    view_w : f64,//场景中的渲染面的宽 由fov_w和n运算得出
    view_h : f64,//场景中的渲染面的高 由宽高比算出
    
    vw_step : f64,//每个像素需要移动的场景里的大小,缓存用,方便调用
    vh_step : f64,

    sample_pre_pixel:u64, //采样率
    ray_deepth:u64,//最大光线弹射次数
}

impl Camera {
    pub fn get_w(&self) -> u32{
        self.screen_w
    }

    pub fn get_h(&self) -> u32{
        self.screen_h
    }

    pub fn get_view_w(&self) -> f64{
        self.view_w
    }

    pub fn get_view_h(&self) -> f64{
        self.view_h
    }

    pub fn get_pos(&self) -> Vector3{
        self.pos
    }

    pub fn get_piexl_view_pos_start_pos(&self) -> Vector3{
        let veiew_center = self.pos + self.dir * self.near;
        let view_start_pos = veiew_center 
            - self.dir_up * (self.view_h * (0.5f64 + self.vh_step * 0.5)) 
            - self.dir_w * (self.view_w * (0.5f64 + self.vw_step * 0.5));
        return view_start_pos;
    }

    ///render scene with camera
    pub fn render(&self, s:Scene, depth:u64) -> Img{
        let sw = self.get_w();
        let sh = self.get_h();

        let mut img = rust_tiny_img::img::Img::new(sw, sh);

        println!("start render");

        let start_pos = self.get_piexl_view_pos_start_pos();

        let shared_s = Arc::new(s);
        let shared_c = Arc::new(self.clone());
        let shared_img = Arc::new(Mutex::new(img));

        let mut thread_num = 0;
        let shared_thread_num = Arc::new(Mutex::new(thread_num));

        let mut std_out: BufWriter<io::Stdout> = BufWriter::new(io::stdout());
        for h in 0..sh {
            let depth_copy = depth;
            let shared_s_clone = Arc::clone(&shared_s);
            let shared_c_clone = Arc::clone(&shared_c);
            let shared_img_clone = Arc::clone(&shared_img);
            let shared_thread_num_clone = Arc::clone(&shared_thread_num);

            let handle = thread::spawn(move || {
                let mut rng = thread_rng();
                {
                    let mut num = shared_thread_num_clone.lock().unwrap();
                    *num += 1;
                }
                for w in 0..sw {
                    let start_pos = &shared_c_clone.get_piexl_view_pos_start_pos();
                    let color = match shared_c_clone.sample_pre_pixel > 1 {
                        true => {
                            let mut out_vector3 = Vector3::new(0.0, 0.0, 0.0);
                            let rate: f64 = 255.0 / shared_c_clone.sample_pre_pixel as f64;
                            for i in 0..shared_c_clone.sample_pre_pixel {
                                let ray = Camera::get_ray(&shared_c_clone,start_pos,w,h,&mut rng);
                                let vector3 = ray.get_color(&shared_s_clone,depth);
                                out_vector3 += vector3;
                            }
                            out_vector3 *= rate;
                            let cr = out_vector3.get_x() as u8;
                            let cg = out_vector3.get_y() as u8;
                            let cb = out_vector3.get_z() as u8;
                            Color::new(cr, cg, cb, 255u8)
                        },
                        false => {
                            let dir = shared_c_clone.get_piexl_view_pos(start_pos,w,h);
                            let pos = shared_c_clone.pos;
                            let ray = Ray::new(pos,dir - pos);
                            let out_vector3 = ray.get_color(&shared_s_clone,depth_copy) * 255.0;
                            let cr = out_vector3.get_x() as u8;
                            let cg = out_vector3.get_y() as u8;
                            let cb = out_vector3.get_z() as u8;
                            Color::new(cr, cg, cb, 255u8)
                        }
                    };
                    match shared_img_clone.lock().unwrap().set_pixel(w, h, color) {
                        Err(msg) => {
                            panic!("{}",msg);
                        },
                        _ => (),
                    }
                }
                {
                    let mut num = shared_thread_num_clone.lock().unwrap();
                    *num -= 1;
                }
            }); 

            //大于16个线程时
            while *shared_thread_num.lock().unwrap() > 16 {

            }
            crate::rt_lib::show_progress(&mut std_out, (h as f64  * 100.0/ sh as f64 ).ceil());
        }
        while *shared_thread_num.lock().unwrap() > 0 {

        }
        let img = (*shared_img.lock().unwrap()).clone();
        img
    }

    ///获取像素点对应的位置 可以用来生成Ray
    fn get_piexl_view_pos(&self, view_start_pos:&Vector3, x:u32, y:u32) -> Vector3{
        view_start_pos.clone() 
            + self.dir_up * self.vh_step * y as f64 
            + self.dir_w * self.vw_step * x as f64
    }

    fn get_ray(&self, start_pos:&Vector3, x:u32, y:u32, rng:&mut ThreadRng) -> Ray {
        let random_x = rng.gen_range(-0.5f64..0.5f64);
        let random_y = rng.gen_range(-0.5f64..0.5f64);

        let dir = start_pos.clone() 
            + self.dir_up * self.vh_step * (y as f64 + random_y)  
            + self.dir_w * self.vw_step * (x as f64 + random_x) ;
        let ray = Ray::new(self.get_pos(),dir - self.get_pos());
        return ray;
    }
}

这里运用了多线程技术来加速渲染;使用Arc<T>类型来跨线程引用计数,Mutex<T>来定义互斥量。

其中,在一帧中场景和相机是静态的,所以不需要互斥锁;而当前线程数和输出的img是不可同时被多个线程同时访问的。于是对于这个行列渲染像素的循环,我把每个行都拆成了一个单独的线程。

rust没有原生的线程池和作业调度系统,所以这里用了个当前线程数来确保不会创建过多的线程,当shared_thread_num大于16时,使用while循环阻塞住主线程。等后续学习了异步库和线程复用,也许可以改进下这里。

大概能有10倍左右的效率提升。

Sphere,球体可以说是最简单的几何体之一了,圆心和半径就足够描述它。

它被击中的逻辑是:由直线和球的方程设未知数t,使得t满足直线和球的表达式,联立方程,求解此一元二次方程,有正实数解说明就相交了。取其中小的那个结果就是第一次射线与物体表面交互。击中后,会产生一个HitResult,这个结果会生成一条新的Ray。

Tip:这里还会有浮点数的精度问题,当Ray的起点有可能在反弹的面的背面一点点的位置,所以这里会丢弃足够小的t。

pub struct Sphere{
    r:f64,
    pos:Vector3,
    mat:Arc<dyn Mat + Send + Sync>,
}

impl Hitable for Sphere {
    fn try_hit(&self,ray:&Ray) -> Option<HitResult> {
        let cq = self.pos - *ray.get_ori();
        let a = ray.get_dir().length_sqr();
        let b = (ray.get_dir() * -2.0f64).dot(&cq);
        let c = cq.dot(&cq) - self.r * self.r;
        let discriminant  = b * b - 4.0 * a * c;
        match discriminant < 0.0 {
            true => Option::None,
            false => {
                let sqrtd = discriminant.sqrt();
                let mut t = (-b - sqrtd)/(a * 2.0);
                if t < 1e-5 {
                    t = (-b + sqrtd)/(a * 2.0);
                }
                if t < 1e-5 {
                    return Option::None;
                }
                let hit_pos = ray.at(t);
                let mut normal = (hit_pos - self.pos) / self.r;
                normal.normallize();
                let mat = Arc::clone(&self.mat);
                Option::Some(HitResult::new(ray,t,hit_pos,normal, &mat))
            },
        }
    }
}

在开发早期,颜色用法线简单代表,结果如图:

这个小渲染器的基本结构就是这样了,接下来打算再写下材质相关的东西。

评论

  1. zhoyo
    9月前
    2023-12-27 20:06:08

    厉害厉害

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇