当前位置: 首页 > ops >正文

初始图形学(11)

今日收官之战,图形学之旅差不多到此为止了

可定位相机

我们的相机目前是固定视角的,比较单调。由于程序的复杂性,所以在这里我们最后实现它。首先,我们需要为我们的相机添加视场(fov)效果。这是从渲染图像一边到另外一边的视觉角度。由于我们的图像不是正方形视图,所以水平和垂直的fov是不同的。这里我们选择开发垂直fov。我们用度数来指定它,然后再构造函数中将其转换为弧度。

计算机视几何

我们继续保持从原点发出的光线,指向z轴上的平面,然后使h与这个距离的比例保持一致:

image.png

其中theta是我们的视野,即fov,这里表示h = tan(theta/2)

我们将其应用到我们的相机类中:

class camera{
public:double aspect_radio = 1.0;      //图像的宽高比int    image_width = 100;       //图像宽度的像素数int    samples_per_pixel = 10;  //每个像素的采样次数int    max_depth = 10;          //光线追踪的最大递归深度
​double vfov = 90;               //垂直视角(视场)...
private:...void initialize(){image_height = int(image_width/aspect_radio);image_height = (image_height < 1) ? 1 : image_height;
​pixel_samples_scale = 1.0 / samples_per_pixel;
​camera_center = point3 (0,0,0);
​//确认视窗的设置auto focal_length = 1.0;    //焦距设置auto theta = degree_to_radius(vfov);auto h = std::tan(theta/2);auto viewport_height = 2*h*focal_length;auto viewport_width = viewport_height*(double (image_width)/image_height);
​//视图边缘的向量计算auto viewport_u = vec3(viewport_width,0,0);auto viewport_v = vec3(0,-viewport_height,0);//计算视图的像素间的水平竖直增量pixel_delta_u = viewport_u/image_width;pixel_delta_v = viewport_v/image_height;
​//计算左上角第一个像素中心的坐标auto viewport_upper_left = camera_center - vec3(0,0,focal_length) - viewport_v/2 - viewport_u/2;pixel00_loc = viewport_upper_left + 0.5*(pixel_delta_u+pixel_delta_v);}...
};

这个原理就是实现广角,图像的宽高比不变,但是视口的大小改变了,使得图像有拉伸压缩的视觉效果

我们用广角来渲染一下我们之前的图片试试,发现渲染出来的效果和之前是一样的,这是为什么呢? 因为之前设置的视口高度为2.0,而焦距为1.0,所以计算可以得到当时的视角也是90°,自然和现在是一样的了,同理我们可以通过缩小垂直视野来看到更远处的东西,这就是放大缩小的原理

摄像机的定位和定向

如果我们想要任意摆放,获得任意的视角,我们首先需要确定两个点,一个是我们放置计算机的点lookfrom,还有一个是我们想要看到点lookat

然后我们还需要定义一个方向作为摄像机的倾斜角度,即lookat-lookfrom轴的旋转。但其实还有一种方法,我们保持lookfromlookat两点不变,然后定义一个向上的方向向量,作为我们的摄像方向。

image.png

我们可以指定任何我们想要的向上向量,只要它不与视图方向平行。将这个向上向量投影到与视图方向垂直的平面上,以获得相对于摄像机的向上向量。我们将其命名为vup即向上向量。我们可以通过叉乘和向量的单位化,得到一个完整的正交归一基,然后我们就可以用(u,v,w)来描述摄像机的方向了。

这里u指的是摄像机右侧的单位向量,v指的是摄像机向上的单位向量,w指的是视图方向相反的单位向量,摄像机中心位于原点

image.png

之前我们需要让-Z和-w在同一水平面,以实现水平摄像叫角度,现在我们只需要指定我们的向上向量vup指向(0,1,0)就可以保持摄像机的水平了

我们将这些功能加入相机类中:

class camera{
public:double aspect_radio = 1.0;      //图像的宽高比int    image_width = 100;       //图像宽度的像素数int    samples_per_pixel = 10;  //每个像素的采样次数int    max_depth = 10;          //光线追踪的最大递归深度
​double vfov = 90;               //垂直视角(视场)point3 lookfrom = point3 (0,0,0);   //相机位置point3 lookat = point3 (0,0,-1);    //观察点vec3   vup = vec3(0,1,0);           //相机相对向上的位置...
private:int image_height;           //渲染图像的高度double pixel_samples_scale; //每次采样的颜色权重point3 camera_center;       //相机的中心point3 pixel00_loc;         //像素(0,0)的位置vec3 pixel_delta_u;         //向右的偏移值vec3 pixel_delta_v;         //向下的偏移值vec3 u,v,w;                 //相机的相对坐标系
​void initialize(){image_height = int(image_width/aspect_radio);image_height = (image_height < 1) ? 1 : image_height;
​pixel_samples_scale = 1.0 / samples_per_pixel;
​camera_center = lookfrom;
​//确认视窗的设置auto focal_length = (lookfrom - lookat).length();    //焦距设置auto theta = degree_to_radius(vfov);auto h = std::tan(theta/2);auto viewport_height = 2*h*focal_length;auto viewport_width = viewport_height*(double (image_width)/image_height);
​//计算摄像机的相对基底w = unit_vector(lookfrom-lookat);u = unit_vector(cross(vup,w));v = cross(w,u);;
​//视图边缘的向量计算auto viewport_u = viewport_width * u;auto viewport_v = viewport_height * -v;//计算视图的像素间的水平竖直增量pixel_delta_u = viewport_u/image_width;pixel_delta_v = viewport_v/image_height;
​//计算左上角第一个像素中心的坐标auto viewport_upper_left = camera_center - (focal_length * w) - viewport_v/2 - viewport_u/2;pixel00_loc = viewport_upper_left + 0.5*(pixel_delta_u+pixel_delta_v);}...
};

然后我们在main函数中调整我们的视角:

int main(){...cam.vfov = 90;cam.lookfrom = point3 (-2,2,1);cam.lookat = point3 (0,0,1);cam.vup = vec3 (0,1,0);...
}

image.png

然后我们发现渲染出来的图片不是很清晰,我们可以通过修改垂直视野来放大

cam.vfov = 20;

image.png

太好看了

散焦模糊

最后,我们还需要实现摄像机的一个特性:散焦模糊。在摄影中,我们把这种效果称之为景深。

在真实相机中,散焦模糊的产生是因为相机需要一个较大的孔(光圈)来收集光线,而不是一个针孔。如果只有一个针孔,那么所有物体都会清晰地成像,但进入相机的光线会非常少,导致曝光不足。所以相机会在胶片/传感器前面加一个镜头,那么会有一个特定的距离,在这个距离上看到的物体都是清晰的,然后离这个距离越远,图像就越模糊。你可以这么理解镜头:所有从焦点距离的特定点发出的光线——并且击中镜头——都会弯曲回图像传感器上的一个单一点。

在这里我们需要区分两个概念:

  • 焦距:相机中心到视口的距离,焦距决定了视场的大小。焦距越长,视场越窄。

  • 焦点距离:焦点距离是相机中心到焦点平面的距离,在焦点平面上的所有物体看起来都是清晰的

不过这里为了简化模型,我们将焦点平面和视口平面重合。

"光圈"是一个孔,用于控制镜头的有效大小。对于真正的摄像机,如果你需要更多的光线,你会使光圈变大,这将导致远离焦距的物体产生更多的模糊。在我们的虚拟摄像机中,我们可以拥有一个完美的传感器,永远不需要更多的光线,所以我们只在想要产生失焦模糊时使用光圈。

薄透镜近似

真实的相机镜头十分复杂,有传感器,镜头,光圈,胶片等...

image.png

实际上,我们不需要模拟相机内的任何一个部分,这些对于我们而言太过复杂,我们可以简化这个过程。我们将其简化成:我们从一近似平面的圆形"透镜"发出光线,并将它们发送的焦点平面的对应点上(距离透镜focal_length),在这个平面上的3D世界中的所有物体都处于完美的焦点中。

image.png

我们将这个过程展示出来:

  • 焦平面和相机视向垂直

  • 焦距是相机中心与焦点平面之间的距离

  • 视口位于焦点平面上,位于相机视角方向向量为中心

  • 像素位置的网格位于视场内

  • 从当前像素位置周围的区域随机采样(抗锯齿)

  • 相机从镜头上的随机点发射光线,通过图像样本位置

生成样本光线

没有散焦模糊时,所有的场景光线都来自相机中心(lookfrom)。为了实现散焦模糊,我们在相机中心构造一个圆盘。半径越大,散焦模糊越明显。你可以把我们的原始相机想想象成一个半径为0的散焦圆盘,所以完全不模糊。

所以我们将散焦盘的设置作为相机类的一个参数。我们将其半径作为相机系数,同时还需注意一点,相机的焦点距离也会影响散焦模糊的效果。此时,为了控制散焦模糊的程度,可以选择以下两种方式:

  • 散焦圆盘的半径:但是散焦模糊的效果会随着焦点距离的改变而被影响

  • 锥角:指定一个锥角,锥的顶点位于视口中心,底面位于相机中心,我们可以通过计算得到相应的底面半径

由于我们将从失焦盘中选择随机点,我们需要一个函数来完成这个任务random_in_unit_disk()这个和我们在random_in_unit_sphere()用到方法一样,只不过这个是二维的:

//vec3.h
inline vec3 random_in_unit_disk(){while (true){auto p = vec3(random_double(-1,1),random_double(-1,1),0);if(p.length_squared() < 1)return p;}
}

现在我们更新相机,加入失焦模糊的功能:

class camera{
public:double aspect_ratio = 1.0;      //图像的宽高比int    image_width = 100;       //图像宽度的像素数int    samples_per_pixel = 10;  //每个像素的采样次数int    max_depth = 10;          //光线追踪的最大递归深度
​double vfov = 90;               //垂直视角(视场)point3 lookfrom = point3 (0,0,0);   //相机位置point3 lookat = point3 (0,0,-1);    //观察点vec3   vup = vec3(0,1,0);           //相机相对向上的位置
​double defocus_angle = 0;       //锥角double focus_dist = 0;          //从相机中心到焦点平面中心的距离...
private:int image_height;           //渲染图像的高度double pixel_samples_scale; //每次采样的颜色权重point3 camera_center;       //相机的中心point3 pixel00_loc;         //像素(0,0)的位置vec3 pixel_delta_u;         //向右的偏移值vec3 pixel_delta_v;         //向下的偏移值vec3 u,v,w;                 //相机的相对坐标系vec3 defocus_disk_u;        //散焦圆盘水平向量vec3 defocus_disk_v;        //散焦圆盘垂直向量
​void initialize(){image_height = int(image_width/aspect_ratio);image_height = (image_height < 1) ? 1 : image_height;
​pixel_samples_scale = 1.0 / samples_per_pixel;
​camera_center = lookfrom;
​//确认视窗的设置
//        auto focal_length = (lookfrom - lookat).length();    //焦距设置auto theta = degree_to_radius(vfov);auto h = std::tan(theta/2);auto viewport_height = 2*h*focus_dist;   //确保视口和焦点平面重合auto viewport_width = viewport_height*(double (image_width)/image_height);
​//计算摄像机的相对基底w = unit_vector(lookfrom-lookat);u = unit_vector(cross(vup,w));v = cross(w,u);
​//视图边缘的向量计算auto viewport_u = viewport_width * u;auto viewport_v = viewport_height * -v;//计算视图的像素间的水平竖直增量pixel_delta_u = viewport_u/image_width;pixel_delta_v = viewport_v/image_height;
​//计算左上角第一个像素中心的坐标auto viewport_upper_left = camera_center - (focus_dist * w) - viewport_v/2 - viewport_u/2;pixel00_loc = viewport_upper_left + 0.5*(pixel_delta_u+pixel_delta_v);
​//计算相机散焦圆盘的基向量auto defocus_radius = focus_dist * std::tan(degree_to_radius(defocus_angle/2));defocus_disk_u = u*defocus_radius;defocus_disk_v = v*defocus_radius;}
​ray get_ray(int i,int j){//构造一个从散焦圆盘开始的随机采样射线,指向(i,j)像素周围的采样点
​auto offset = sample_square();auto pixel_sample = pixel00_loc + ((i+offset.x())*pixel_delta_u) + ((j+offset.y())*pixel_delta_v);
​auto ray_origin = (defocus_angle <= 0) ? camera_center :defocus_disk_sample();auto ray_direction = pixel_sample - ray_origin;
​return ray(ray_origin,ray_direction);}...point3 defocus_disk_sample() const {// 返回散焦盘的上的随机点auto p = random_in_unit_disk();return camera_center + (p[0]*defocus_disk_u) + (p[1]*defocus_disk_v);}...
};

现在我们的相机具备了景深的效果,然我们来渲染试试效果吧:

image.png

Good,到此为止,我们的相机终于完成了!

http://www.xdnf.cn/news/14882.html

相关文章:

  • 揭秘C++继承机制:从基础到菱形继承全解析----《Hello C++ Wrold!》(13)--(C/C++)
  • 解决jenkins的Exec command命令nohup java -jar不启动问题
  • 每天一个前端小知识 Day 23 - PWA 渐进式 Web 应用开发
  • 异步Websocket构建聊天室
  • 分布式压测
  • 关于 栈帧变化完整流程图(函数嵌套)
  • Apache Spark 4.0:将大数据分析提升到新的水平
  • 【Linux】基础开发工具(1)
  • 【JS逆向基础】数据分析之正则表达式
  • 【java】webservice服务
  • 基于Excel的数据分析思维与分析方法
  • 【Vibe Coding 实战】我如何用 AI 把一张草图变成了能跑的应用
  • Hadoop高可用集群搭建
  • 【排坑记录】Cursor 出现 “Connection failed” 报错?试试修改 HTTP Compatibility Mode!
  • HTTPS 协议原理
  • 数据驱动实时市场动态监测:让商业决策跑赢时间
  • 操作系统王道考研习题
  • CICD[构建镜像]:构建django使用的docker镜像
  • Linux proxy设置
  • 2048小游戏实现
  • PADS交互式布局
  • 查看linux中steam游戏的兼容性
  • Python练习Day1
  • 【Elasticsearch】检索排序 分页
  • vue router 里push方法重写为什么要重绑定this
  • FLUX.1-Kontext 高效训练 LoRA:释放大语言模型定制化潜能的完整指南
  • 相机位姿估计
  • 一文讲清楚React中Refs的应用
  • 成为git砖家(12): 看懂git合并分支时冲突提示符
  • Python 机器学习核心入门与实战进阶 Day 3 - 决策树 随机森林模型实战