目标:
使用vscodeIDE编写代码,这是我的配置
学习教程完成简易的光线追踪器开发
1·输出PPM图像
有一种PPM格式的图像,可以通过自定义数据生成简单的图像
首先p3表明颜色是ASCII码格式,接着指定分辨率,和最大颜色值,下面的都是RGB颜色值 ,
我们可以通过代码设置这些数据,通过执行exe编译后的文件,将cont标准输出流的内容写入.ppm的文件里
首先指定p3和分辨率(200宽100高)和255最大颜色值,然后设置每个像素颜色值,首先在0---1之间,然后转为0---255的颜色
对于PPM来说从左往右,从上到下写入的,因此首先输出的数据是左上,最后输出的是右下
#include <iostream>
int main() {
// Image
int image_width = 256;
int image_height = 100;
// 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';
}
}
}
我们已经有了所有数据,通过新建终端,编译后,执行命令写入命令
最终效果:
好了我们现在可以显示一个简单的图片,我想要计时器,以便更好的调试,为了输出到终端又不影响ppm文件,我们用std::clog << / std::erro<<,打印一层循环(还剩多少行image_height - j) ,目前速度很快
2·VEC3,color文件
让我们抽象出两个工具文件,用vec3`类3个double,using color = vec3;
vec3 color(double(i) / image_width, double(j) / image_height, 0.2);
color.write_color(std::cout);
3·ray类
作为光线追踪器,必不可少的就是光线,光线隐式公式:
光线是一个射线,a原点,t 时间,b方向
让我们做一个蓝天背景吧,首先计算屏幕属性
// Image
auto aspect_ratio = 16.0 / 9.0;
int image_width = 400;
// Calculate the image height, and ensure that it's at least 1.
int image_height = int(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
// Camera
auto focal_length = 1.0;
auto viewport_height = 2.0;
auto viewport_width = viewport_height * (double(image_width)/image_height);
auto camera_center = point3(0, 0, 0);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
auto viewport_u = vec3(viewport_width, 0, 0);
auto viewport_v = vec3(0, -viewport_height, 0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
auto pixel_delta_u = viewport_u / image_width;
auto pixel_delta_v = viewport_v / image_height;
// Calculate the location of the upper left pixel.
auto viewport_upper_left = camera_center
- vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2;
auto pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
然后向每个像素发射光线,通过光线的y值做个简单的线性插值
vec3 ray_color(const ray& r) {/* 根据ray的y值,返回渐变的蓝白色 */
vec3 unit_direction = unit_vector(r.direction());/* 单位化光线 */
auto t = 0.5*(unit_direction.y() + 1.0);/* 1---0 */
return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);/* 从上到下,从蓝到白 */
}
完成效果:
4·渲染球体
接下来我们渲染一个球体吧,使用球体的 隐式表示公式:
这是球心在0,0,0位置的方程,所有球面的点xyz都满足=r^2,如果<r^2则点在内部,否则在外部
如果球心在cx,cy,cz,则
其中r可以写为点p和球心c的向量形式(等价) ,最终的球面方程:
那么光线和球体相交,两个方程都满足,即光线为t时间时的坐标,作为点p满足在球体表面
唯一的未知数就是t,即关于t的一个一元二次方程
通过求根公式判断交点、如果存在返回指定颜色
bool hit_sphere(const vec3 ¢er, double radius, const ray &r)
{
vec3 oc = r.origin() - center;
auto a = dot(r.direction(), r.direction());
auto b = 2.0 * dot(oc, r.direction());
auto c = dot(oc, oc) - radius * radius;
auto discriminant = b * b - 4 * a * c;/* 当 b2−4ac>=0 时,表示二次函数与x轴有至少一个交点 */
return (discriminant > 0);
}
最终效果:
5·着色
我们如何计算这个球体的法线?(交点 - 球心)
根据法线向量为球体着色
auto t = hit_sphere(vec3(0,0,-1), 0.5, r);
if (t > 0.0) {
vec3 N = unit_vector(r.at(t) - vec3(0,0,-1));
return 0.5*vec3(N.x()+1, N.y()+1, N.z()+1);
}
结果:
6·优化,重构代码
现在的main文件有太多杂项,我们把内部抽象出来
项1:求根公式
我们可以把求根公式中的b替换为2h,经过展开移项后
因此,修改后的代码
double hit_sphere(const vec3 ¢er, double radius, const ray &r)
{
vec3 oc = r.origin() - center;
auto a = r.direction().length_squared();
auto half_b = dot(oc, r.direction());
auto c = oc.length_squared() - radius*radius;
auto discriminant = half_b*half_b - a*c;
if (discriminant < 0) {
return -1.0;
} else {
return (-half_b - sqrt(discriminant) ) / a;
}
}
项2:抽象物体继承体系
我们建立一个物体基类叫做hittable,其中STRUCT hit_record记录交点的信息,hit()虚函数用来判断是否有交点
建立一个sphere派生类继承hittable,
为了满足多物体绘制,不要忘记深度测试
项3:法线方向
我们想要物体在内外方向都有法线,因此如果光线和法线dot>0即同方向即光线从球体内部照射,则法线反转,否则不反转
front_face = dot(r.direction(), outward_normal) < 0;/* 光线和法线是否一致 */
normal = front_face ? outward_normal : -outward_normal;/* 不一致反转法线方向 */
项4:物体列表
保存所有物体的vector,每次求交会遍历内部所有物体,如果通过更新交点信息
项5:常用函数和工具
rtweekend.h常用数据和函数类,interval时间间隔类
项6:抽象出camera类
initialize()负责初始化相机,ray_color()计算光线颜色,render()开始渲染世界
现在我们的main文件的大部分内容都抽象到其他文件中了
7·两个球体
利用我们的框架API渲染两个球体吧
hittable_list world;
world.add(make_shared<sphere>(point3(0, 0, -1), 0.5));
world.add(make_shared<sphere>(point3(0, -100.5, -1), 100));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.render(world);
结果:
8·反走样/抗锯齿
MSAA:每个像素增加samples_per_pixel个采样点