学了一段时间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仓库。
接下来输出了一张图片作为测试:
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))
},
}
}
}
在开发早期,颜色用法线简单代表,结果如图:
这个小渲染器的基本结构就是这样了,接下来打算再写下材质相关的东西。
厉害厉害