Loading... 有兴趣而且想自己动手实现的话可以克隆这个REPO [GitHub - DarcJC/RTiOW-Playground: My playground of Ray Tracing in One Weekend](https://github.com/DarcJC/RTiOW-Playground) ,然后把里面的头文件拷出来自己去实现函数定义。 ## 向量类 实现了一些基本运算。没什么好介绍的,写烂了已经( 因为这里没用到齐次坐标,所以用的三维向量(我习惯叫它Vector3D,dimension/double)。 ```cpp // Vector3D.h #include <ostream> // std::ostream class Vector3D; // 省略大量运算的定义还有实现 using Point3D = Vector3D; using Color = Vector3D; // Vector3D.cpp void write_color(std::ostream& out, Color& color) { out << static_cast<int>(255.999 * color.x) << ' ' << static_cast<int>(255.999 * color.y) << ' ' << static_cast<int>(255.999 * color.z) << '\n'; } ``` 其余工具类就不说了。就有一点值得注意,声明为`inline`的代码必须在声明处给出定义(definition),因为编译期内联需要具体实现(就像你要搬东西,你需要找到这个东西具体在哪才能搬。) ## 射线 这里要把射线(Ray)抽象成一个类,并计算沿射线所看到的颜色。(加粗的大写符号是向量/点。) 通过函数 $\bold{P}(t) = \bold{A} + t\bold{b}$ 可以得到一条射线。其中 $\bold{P}(t)$ 是*点的位置关于时间的函数*,注意这里的”点“是指构成射线的无数个点,但在如果参数 $t$ 确定,这个点也是确定的。而 $\bold{A}$ 是射线端点(origin),$\bold{b}$ 为射线方向,参数 $t$ 为实数(在代码中可以使用double表示)。 换句话说,点 $\bold{P}(t)$ 随着时间 $t$ 的变化而在一条确定的直线上移动。当 $t$ 为负值时,该点会越过端点而往反向移动。加入 $t \ge 0$ 的限制条件, 我们就可以得到一条射线了。 现在可以抽象出一个射线类了: ```cpp class Ray { private: Point3D origin; Vector3D direction; public: [[nodiscard]] const Point3D &getOrigin() const; void setOrigin(const Point3D &origin); [[nodiscard]] const Vector3D &getDirection() const; void setDirection(const Vector3D &direction); public: Ray(); Ray(const Point3D& origin, const Vector3D& direction); [[nodiscard]] Point3D at(double t) const; }; ``` ### 发射! 光线追踪的核心就是发射射线,并计算沿射线所能看到的颜色。 RTiOW作者说,用正方形的图像的时候,天天把$x$与$y$搞反,所以索性用$16:9$的宽高比了。 确定了图像的分辨率之后,我们还差一个虚拟视口(viewport)。对于标准的方形像素排布,视口的宽高比应该跟图像的宽高比一致。 这里的视口高度定为$2$个单位长度。而投影平面与投影点的距离定为$1$个单位长度,这个距离我们称为焦距(focal length),注意不是聚焦距离(focus distance)。  如上图,使用的是右手坐标系($z$轴负坐标指向屏幕内),原点就是我们的”眼睛(eye)“,或者说是相机(camera)。 现在可以试试用 **线性混合(linear blend, linear interpolation, lerp在这里都表示一个意思)** 来给出某个像素点的值,我们有如下代码: ```cpp Color ray_color(const Ray& r) { Vector3D unit_direction = unit(r.getDirection()); auto t = 0.5 * (unit_direction.y + 1.0); return (1.0 - t) * Color(1.0, 1.0, 1.0) + t * Color(.5, .7, 1.0); } ``` $t\in [0.0, 1.0]$,令$t=1.0$时颜色值为蓝色,而$t=0.0$时颜色值为白色,我们需要基于$t$来混合这两种颜色,我们有线性插值公式: $$ 插值结果 = (1-t) \cdot 起始值 + t \cdot 结束值 $$ 因为我们事先将整个向量做了归一化,所以这里基于$y$坐标的颜色混合也会在$x$轴有所体现。  ### 来个球球 我们该加个物体进去了。大伙贼喜欢用球体,因为他容易,容易,还是TMD容易做光线命中判定。 #### 射线与球的相交 先回顾一下,一个半径为$R$,位于原点处的球可以使用等式$x^2+y^2+z^2=R^2$表示。换句话说,上面那个等式描述了一个*所有位于球面上的点*的集合。所以,设某个点坐标为$(x,y,z)$,如果点在球的内部,那么有$x^2+y^2+z^2 \lt R^2$,在球上有$x^2+y^2+z^2 = R^2$,在球外有$x^2+y^2+z^2 \gt R^2$。 当然,如果球心在$(C_x,C_y,C_z)$而非原点,那么: $$ (x-C_x)^2+(y-C_y)^2+(z-C_z)^2=r^2 $$ 图形学中,我们更倾向于使用向量进行计算。将从球心$C=(C_x,C_y,C_z)$指向点$P=(x,y,z)$的向量记作$(\bold{P}-\bold{C})$,我们有: $$ (\bold{P}-\bold{C})\cdot(\bold{P}-\bold{C})=(x-C_x)^2+(y-C_y)^2+(z-C_z)^2 $$ 替换一下就得到了向量形式的球体方程: $$ (\bold{P}-\bold{C})\cdot(\bold{P}-\bold{C})=r^2 $$ 我们可以这么讲——“任意满足方程的点$P$都位于球面上”。我们目的是判断射线$\bold{P}(t)=\bold{A}+t\bold{b}$是否“命中”这个球体。如果射线命中了球体,那么在射线方程的定义域内,必然存在$t$使得$\bold{P}(t)$满足: $$ (\bold{P}(t) -\bold{C})\cdot (\bold{P}(t)-\bold{C})=r^2 $$ 其展开形式: $$ (\bold{A}+t\bold{b}-\bold{C})\cdot(\bold{A}+t\bold{b}-\bold{C})=r^2 $$ 将展开式再次展开并移项: $$ t^2\bold{b}\cdot \bold{b}+2t\bold{b}\cdot(\bold{A}-\bold{C})+(\bold{A}-\bold{C})\cdot(\bold{A}-\bold{C})-r^2=0 $$ 我们得到了一个一元二次方程,芜湖。这时候可以祭出一元二次方程的根判别式($b^2-4ac$): $$ =[2\bold{b}\cdot(\bold{A}-\bold{C})]^2-4(\bold{b}\cdot\bold{b})[(\bold{A}-\bold{C})\cdot(\bold{A}-\bold{C})-r^2] $$ 判别式$\lt 0$时,不存在实数根;判别式$=0$时存在一个实数根;判别式$>0$时存在两个实数根。  #### 追着球的光 我们可以硬编码一个球进去了,如果光线命中了球体,那么对应像素的颜色我们将设置为红色。 加个判定: ```cpp bool hit_sphere(const Point3D& center, double radius, const Ray& r) { const auto a_minus_c = r.getOrigin() - center; const auto a = dot(r.getDirection(), r.getDirection()); const auto b = dot(2.0 * r.getDirection(), a_minus_c); const auto c = dot(a_minus_c, a_minus_c) - (radius * radius); const auto discriminant = b * b - (4 * a * c); return discriminant >= .0; } Color ray_color(const Ray& r) { if (hit_sphere(Point3D(0, 0, -1), 0.5, r)) { return {1, 0, 0}; } Vector3D unit_direction = unit(r.getDirection()); auto t = 0.5 * (unit_direction.y + 1.0); return (1.0 - t) * Color(1.0, 1.0, 1.0) + t * Color(.5, .7, 1.0); } ``` 这个代码有一个问题——它默认$t\in ℝ^+$。如果我们令球心的$z$轴为$+1$,也就是使球在摄像机的背后,依然可以得到相同的图像。 当然,我们还需要考虑多个物体,以及它们之间的阴影和反射。 最后修改:2022 年 01 月 08 日 03 : 16 PM © 允许规范转载 赞赏 请我喝杯咖啡 ×Close 赞赏作者 扫一扫支付 支付宝支付 微信支付