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

GAMES101作业7及课程总结(重点实现多线程加速,微表面模型材质)

目录

  • 闲言碎语
  • 最终全部效果展示(均为1024×1024×512ssp)
  • 课程总结与理解(Path Tracing)
  • 框架梳理
  • 任务一:迁移相关代码
  • 任务二:实现path tracing
  • 任务三:多线程加速(包括其他加速的小trick)
    • 1.随机数构造器优化
    • 2.多线程加速
    • 3.debug改成release版本
  • 任务四:微表面模型
    • 1. 微表面材质定义
    • 2. 微表面分布的表达
      • 2.1. D(h):法线分布函数(**Normal Distribution Fuction,NDF**)
      • 2.1. G(i,o,h):几何遮挡函数(**Shadowing and Making term**)
    • 3.光在每个微表面散射的BRDF
    • 4.Torrance-Sparrow Model
    • 5.代码实现
  • 任务五:完美反射模型(看到顺便实现了,作业没要求)
  • 代码实现可能遇到的问题
    • 1.光源融入到整个天花板,没有出现黑色
    • 2.箱子背阴处有很多黑色噪点
    • 3.有白色噪点(ssp调高仍然有)
  • 理论思考
    • 1.偏移为什么要分两种情况这么偏移?
    • 2.为什么path tracing有软阴影?
    • 3.把witted style和path tracing的结果对比一下,两者的实现的不同点有哪些?
    • 4.为什么程序中,渲染方程的入射radiance是光源的emit,是一个常量?
    • 5.为什么说光栅化比光追快?
  • 个人感悟
  • 参考资料

闲言碎语

前言:从这篇文章开始,转知乎了,感觉知乎的社区氛围更好,emmm,哈哈哈主要还是想接触一下知乎的图形学大佬们。知乎账号链接:https://www.zhihu.com/people/xuu-27-24

emmm,距离完成作业6已经过去了一个月多的时间(我承认中间有段时间在摆烂==),怎么说呢,感觉作业7真的知识体系非常庞大,虽然实现的代码量不多,但是里面真的很多细节很多trick,对我这种小菜鸡感觉还是有挺大难度的。。。再加上感觉自己挺钻牛角尖的,所以搞了挺久的,也最终算是完成了作业7的全部要求,害,希望后面本科毕设不要太赶吧。

提个建议:如果遇到实在解决不了的问题,可以上games论坛看看,里面有很多共性的问题,上面的问题我几乎都遇到了。。。

最终全部效果展示(均为1024×1024×512ssp)

基础path tracing实现(作业基本要求)
在这里插入图片描述
微表面材质
在这里插入图片描述
完美镜面反射材质
在这里插入图片描述

课程总结与理解(Path Tracing)

课程方面,主要是先讲辐射度量学,然后推导出渲染方程,之后介绍用来求解积分的蒙特卡洛方法,最后讲如何在程序中求解渲染方程计算颜色(包括很多的trick细节,比如对光源进行直接采样提高采样效率,俄罗斯轮盘赌解决无限递归等等),从而实现path tracing。

path tracing流程:整体的渲染流程实际上更多的是在Whitted-Style上的改进,前期添加物体,构建包围盒,这些都是一样的。不一样的地方在于path tracing在最终着色的时候本质上是求解一个渲染方程,需要做大量的采样,近似积分结果,里面小trick非常多。而Whitted-Style并没有做采样求积分,就是简单的光线反射和折射,无法像path tracing一样考虑全局光照。 总体上来说,就是每个像素点发出多条光线,每条光线会通过之前构建的包围盒找到与场景中物体的第一个交点,然后根据渲染方程计算该交点的颜色,并返回,最后将全部发射的光线的颜色求平均,就得到该像素的颜色。

其实这个渲染方程本身的原理还是挺复杂的,可以研究的很深入,尤其辐射度量学那块,课程本身其实讲的并不是很细,如果想细致了解的话可以去翻虎书,虎书可以说对辐射度量学进行了深度的解刨,直接从光子开始讲。。。

框架梳理

和作业6使用的框架其实差不多,可以参考我之前写的https://blog.csdn.net/Xuuuuuuuuuuu/article/details/128556319

作业7的框架实际上主要就是castRay函数有所不同,因为path tracing的最终实现是在castRay中实现。

任务一:迁移相关代码

之前写的在这里:https://blog.csdn.net/Xuuuuuuuuuuu/article/details/128556319,把相关的代码贴进去就行。其中要注意的一个点,IntersectP中的等于号一定要去掉,要不然之后会出现部分物体不可见的情况,去掉的位置在这里,见下图:

在这里插入图片描述
为什么这里要去掉等于号,之前闫老师说,图形学编程中很少考虑等于号的情况,几乎是可以忽略的。这是因为这次的场景比较特殊,以右边的绿色墙壁为例,实际上它的Boundbox就是一个与某一轴平行长方形(不是长方体,不信的话可以printBoundbox的那两个点出来看看)所以光线和这个Boundbox相交检测的时候,算出来的t_enter和t_exit是相等的。此时如果相等时仍判断为不相交,就会出错,此时部分物体不可见,如下图:

在这里插入图片描述

任务二:实现path tracing

代码量也不是很大,主要就是跟着作业中给的伪代码照着敲,但是想呈现出最终的结果有很多小细节要注意,其中这里面各个向量的方向问题就很容易搞错,一定要仔细琢磨清楚。因为里面要修的细节太多了,我这边直接给出任务二完整的代码(个人建议不要完全照抄,可以自己先跟着作业提供的伪代码敲一下,遇到问题再去自己想办法找资料解决,在最终得到想要的结果后,你会发现这个过程非常值得。),具体的很多细节问题放到后文讨论。很多细节的问题建议直接看代码,可以理解的更清楚。

Vector3f Scene::castRay(const Ray& ray, int depth) const
{// TO DO Implement Path Tracing Algorithm hereVector3f hitColor = this->backgroundColor;Intersection shade_point_inter = Scene::intersect(ray);if (shade_point_inter.happened){Vector3f p = shade_point_inter.coords;Vector3f wo = ray.direction;Vector3f N = shade_point_inter.normal;Vector3f L_dir(0), L_indir(0);//sampleLight(inter,pdf_light)Intersection light_point_inter;float pdf_light;sampleLight(light_point_inter, pdf_light);//Get x,ws,NN,emit from interVector3f x = light_point_inter.coords;Vector3f ws = normalize(x-p);Vector3f NN = light_point_inter.normal;Vector3f emit = light_point_inter.emit;float distance_pTox = (x - p).norm();//Shoot a ray from p to xVector3f p_deviation = (dotProduct(ray.direction, N) < 0) ?p + N * EPSILON :p - N * EPSILON ;Ray ray_pTox(p_deviation, ws);//If the ray is not blocked in the middleffIntersection blocked_point_inter = Scene::intersect(ray_pTox);if (abs(distance_pTox - blocked_point_inter.distance < 0.01 )){L_dir = emit * shade_point_inter.m->eval(wo, ws, N) * dotProduct(ws, N) * dotProduct(-ws, NN) / (distance_pTox * distance_pTox * pdf_light);}//Test Russian Roulette with probability RussianRouolettefloat ksi = get_random_float();if (ksi < RussianRoulette){//wi=sample(wo,N)Vector3f wi = normalize(shade_point_inter.m->sample(wo, N));//Trace a ray r(p,wi)Ray ray_pTowi(p_deviation, wi);//If ray r hit a non-emitting object at qIntersection bounce_point_inter = Scene::intersect(ray_pTowi);if (bounce_point_inter.happened && !bounce_point_inter.m->hasEmission()){float pdf = shade_point_inter.m->pdf(wo, wi, N);if(pdf> EPSILON)L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf *RussianRoulette);}}hitColor = shade_point_inter.m->getEmission() + L_dir + L_indir;}return hitColor;
}

应该能得到下图所示的结果(1024×1024×512ssp):在这里插入图片描述

任务三:多线程加速(包括其他加速的小trick)

我觉得加速是渲染中一个非常重要的点,也是我个人比较感兴趣的点。tmd没有加速之前,代码的运行速度简直惨不忍睹。。。一开始1024×1024×512ssp的图渲染了七八个小时才渲染了百分之20(渲染到百分之20我就直接放弃了==)。。。最终经过不懈努力,总算把速度优化到了40分钟,可以说快了几乎60-80倍。渲染速度的提高可以说极大地帮助了后面的调试!!!

按照下面三步逐步进行优化,我渲染一张512×512×8ssp的图的速度从13分钟提到了10秒钟

1.随机数构造器优化

使用C++的性能分析器:https://blog.csdn.net/u011942101/article/details/123656944

不难发现,主要是求交和采样两部分花了很多的时间,求交这部分本来就耗时很长,里面很多递归。但是为啥,采样这部分要这么长时间?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nHT8YMY1-1676280691347)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20230120142906735.png)]

最后发现是这个函数,特别耗时。一分析是因为构建开销过大,实际上把里面三个变量都定义成static就行(只会初始化一次,避免重复构建),没必要重新构建。经测试,改成static后,512×512×8ssp的渲染速度从13分钟提到了5分钟。

在这里插入图片描述
参考:https://games-cn.org/forums/topic/zuoyeqidexingnengpingjingshisuijishushengcheng/

2.多线程加速

使用多线程加速之前,问自己一个问题,为什么这里可以使用多线程加速?因为这里满足两个条件:1.这个场景下程序可以写成多线程,2.电脑多核。

实际上实现多线程加速不是很难,因为整个场景太适合写成多线程加速了。。。就每个线程分别独立一定数量计算像素颜色就行。原则上就是要最大化每个线程的计算量,对整个frame计算使用多线程。 具体使用过的是C++里面的thread进行多线程,代码如下:

// The main render function. This where we iterate over all pixels in the image,
// generate primary rays and cast these rays into the scene. The content of the
// framebuffer is saved to a file.
void Renderer::Render(const Scene& scene)
{std::vector<Vector3f> framebuffer(scene.width * scene.height);float scale = tan(deg2rad(scene.fov * 0.5));float imageAspectRatio = scene.width / (float)scene.height;Vector3f eye_pos(278, 273, -800);int m = 0;// change the spp value to change sample ammountint spp = 512;//16int thread_num = 8;//我的电脑有8核,所以开8个线程。注:屏幕的高度一定要是线程数的倍数int thread_height = scene.height / thread_num;std::vector<std::thread> threads(thread_num);std::cout << "SPP: " << spp << "\n";//多线程实现std::mutex mtx;float process=0;float Reciprocal_Scene_height=1.f/ (float)scene.height;auto castRay = [&](int thread_index) {int height = thread_height * (thread_index + 1);for (uint32_t j = height - thread_height; j < height; j++){for (uint32_t i = 0; i < scene.width; ++i) {// generate primary ray directionfloat x = (2 * (i + 0.5) / (float)scene.width - 1) *imageAspectRatio * scale;float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;//eye的位置对结果有影响Vector3f dir = normalize(Vector3f(-x, y, 1));for (int k = 0; k < spp; k++){framebuffer[j*scene.width+i] += scene.castRay(Ray(eye_pos, dir), 0) / spp;  }}mtx.lock();process = process + Reciprocal_Scene_height;UpdateProgress(process);mtx.unlock();}};for (int k = 0; k < thread_num; k++){threads[k] = std::thread(castRay,k);}for (int k = 0; k < thread_num; k++){threads[k].join();}UpdateProgress(1.f);// save framebuffer to fileFILE* fp = fopen("binary.ppm", "wb");(void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);for (auto i = 0; i < scene.height * scene.width; ++i) {static unsigned char color[3];color[0] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].x), 0.6f));color[1] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].y), 0.6f));color[2] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].z), 0.6f));fwrite(color, 1, 3, fp);}fclose(fp);    
}

512×512×8ssp的渲染速度从5分钟提到了1分钟!!!
真的很担心cpu被烧坏。。。这种情况下说明多线程编程成功,CPU被疯狂地调用。

在这里插入图片描述

3.debug改成release版本

无心之举,512×512×8ssp的渲染速度从1分钟提到了10秒钟。。。 只能说release比debug版本快了真的不止一点半点。

任务四:微表面模型

101讲的不是很细,因此我就花了不少时间仔细地看了下202和一些其他资料。

参考1:https://zhuanlan.zhihu.com/p/434964126(写得贼nb,推导地很深入)

参考2:https://zhuanlan.zhihu.com/p/152226698(这个更加全面一点,涉及的更广,但1更深)

本质上微表面材质是描述高光项,真正的brdf是由漫反射项+高光项组成的,如下(参考2):

在这里插入图片描述

各自的比例是由菲涅尔项计算的,而菲涅尔项是折射和反射的比例,这里漫反射等价于折射(因为漫反射从微观角度是折射进入物体,然后多次反弹弹出表面,产生漫反射现象)。

说白了就是一部分算折射然后反弹出来的能量,一部分算反射出来的能量。

在这里插入图片描述

1. 微表面材质定义

说白了,就是把一个物体表面看作有多个微表面组成的表面,从而展示更加真实的材质细节。

下图的虚线就是物体表面,而对其着色的时候微表面模型会将物体表面近似成下图实线尖尖样子进行着色,具体就是通过定义微表面模型的prdf实现的。

思想和凹凸贴图那边有点想,实际上没有改变物体的几何模型,但是通过改变了prdf,使物体有了多个微表面组成的感觉。所以有了这句话:远处看是材质和外观,近处看是几何。

在这里插入图片描述

以太空图为例,远处看一个物体的时候看不到很微小的东西,看到的是一个全局效果。

在这里插入图片描述

ok,上面主要大致讲微表面模型大概是个什么东西,下面就是具体深入的理解,

微表面模型主要分成两个部分(也就是说设计一个微表面模型就必须要考虑到这两部分):

①微表面分布的表达(全局,比如法线分布、几何遮挡等)

②光在每个微表面散射的BRDF(细节,单个微表面的材质,即prdf,可以是完美镜面反射,又可以是理想漫反射)

接下来依次介绍以上两部分,然后再介绍最常见的Torrance-Sparrow模型。

2. 微表面分布的表达

包括D(h)和G(i,o,h)两块,即法线分布和几何遮挡。

2.1. D(h):法线分布函数(Normal Distribution Fuction,NDF

不同的法线分布会产生完全不一样的材质。

在这里插入图片描述

D(h)本质上是面积微元dA上的法向为h的全部微表面的面积和面积微元dA比例

注:dA是宏观表面上的面积微元。

不难发现,D(h)实际上是针对物体宏观表面上的一个面积微元的,而不是整个物体。

在这里插入图片描述

D(h)的推导公式

在这里插入图片描述

①beckmann NDF

类似高斯分布函数,里面就是有那两个量,α应该是超参数,控制整个分布的宽瘦,θ就是h代进去计算得到的。为什么这么去设计这个函数,是有它的意义在里面。

在这里插入图片描述

②GGX NDF

相较于Beckmann NDF,有了个长尾巴,意味着会有更好的过渡。

在这里插入图片描述

在这里插入图片描述

2.1. G(i,o,h):几何遮挡函数(Shadowing and Making term

描述微表面之间互相遮挡的几何现象的数学模型,本质上是法向为h的微表面在w方向上可见的比例(因为有部分被遮挡了,见下面第一张图)

遮挡项,因为光线或者视线非常平(第二张图的球边界,此时角度称为grazing angle),f的分母会非常小,因此f就会非常大,边界就会非常亮,此时就要引入遮挡项解决这个问题,进行衰减,因为这种情况下,是最容易出现shadowing-masking现象的。

具体推导可以看上述的链接,讲的非常清楚!!!

在这里插入图片描述

在这里插入图片描述

3.光在每个微表面散射的BRDF

这里又可以有很多文章可以做,可以把每个微表面看作理想镜面(完美反射),也可以看成理想玻璃表面(又有折射又有反射),还可以看成漫反射表面。

结合上述两个微表面的分布项,再定义每个微表面的材质,就能推导出相关的微表面模型。

下文以Torrance-Sparrow Model为例。

4.Torrance-Sparrow Model

把每个微表面看成理想镜面。

推导过程看不懂,我太菜了,就这样吧,该章节一开始的链接里有。

在这里插入图片描述

5.代码实现

实现的是上述的Torrance-Sparrow Model,F项是代码中已经写好的菲涅尔项,D项和G项选择GGX,如下图所示:
在这里插入图片描述

在这里插入图片描述
其中各参数的含义可以查看原文https://zhuanlan.zhihu.com/p/434964126,或者可以直接看代码,应该也能够理解。代码实现方面,先在定义里面添加Microfacet材质,如下:

enum MaterialType { DIFFUSE, MICROFACET}

然后在main函数中添加物体,并对其赋予Microfacet材质,如下:

    Material* microfacet = new Material(MICROFACET, Vector3f(0.0f));microfacet->Ks = Vector3f(0.45, 0.45, 0.45);microfacet->Kd = Vector3f(0.3, 0.3, 0.25);microfacet->ior = 12.85;Sphere sphere1(Vector3f(150, 100, 200), 100, microfacet);scene.Add(&sphere1);

sample和pdf直接照抄原本的就行。最后修改eval,在其中实现Torrance-Sparrow Model模型即可,如下:

Vector3f Material::eval(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){switch(m_type){case DIFFUSE:{// calculate the contribution of diffuse modelfloat cosalpha = dotProduct(N, wo);if (cosalpha > 0.0f) {Vector3f diffuse = Kd / M_PI;return diffuse;}elsereturn Vector3f(0.0f);break;}case MICROFACET:{float cosalpha = dotProduct(N, wo);if (cosalpha > 0.0f) {// calculate the contribution of Microfacet modelfloat F, G, D;fresnel(wi, N, ior, F);float Roughness = 1;//超参数,控制粗糙度auto G_function = [&](const float& Roughness, const Vector3f& wi, const Vector3f& wo, const Vector3f& N){float A_wi, A_wo;A_wi = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wi, N))), 2))) / 2;A_wo = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wo, N))), 2))) / 2;float divisor = (1 + A_wi + A_wo);if (divisor < 0.001)return 1.f;elsereturn 1.0f / divisor;};G = G_function(Roughness, -wi, wo, N);auto D_function = [&](const float& Roughness, const Vector3f& h, const Vector3f& N){float cos_sita = dotProduct(h, N);float divisor = (M_PI * pow(1.0 + cos_sita * cos_sita * (Roughness * Roughness - 1), 2));if (divisor < 0.001)return 1.f;else return (Roughness * Roughness) / divisor;};Vector3f h = normalize(-wi + wo);D = D_function(Roughness, h, N);// energy balanceVector3f diffuse = (Vector3f(1.0f) - F) * Kd / M_PI;Vector3f specular;float divisor= ((4 * (dotProduct(N, -wi)) * (dotProduct(N, wo))));if (divisor < 0.001)specular= Vector3f(1);elsespecular = F *G * D / divisor;//std::cout << "F:"<<F << "\n";//std::cout << "diffuse:"<<diffuse<<"\n";//std::cout << "specular:" << specular << "\n";return diffuse+specular;}elsereturn Vector3f(0.0f);break;}}
}

最终效果(均为1024×1024×512ssp)
在这里插入图片描述
如果黑色噪点很多,尝试修改一下sphere的求交判断阈值,有可能是精度的问题,如下:

    Intersection getIntersection(Ray ray){Intersection result;result.happened = false;Vector3f L = ray.origin - center;float a = dotProduct(ray.direction, ray.direction);float b = 2 * dotProduct(ray.direction, L);float c = dotProduct(L, L) - radius2;float t0, t1;if (!solveQuadratic(a, b, c, t0, t1)) return result;if (t0 < 0) t0 = t1;if (t0 < 0) return result;if (t0 > 0.5){result.happened = true;result.coords = Vector3f(ray.origin + ray.direction * t0);result.normal = normalize(Vector3f(result.coords - center));result.m = this->m;result.obj = this;result.distance = t0;}return result;}

如果白色噪点很多,在castRay函数的最后,限制一下hitColor的范围,避免出现特别大的值,影响采样质量。如下:

        hitColor = shade_point_inter.m->getEmission()+L_dir + L_indir;hitColor.x = (clamp(0, 1, hitColor.x));hitColor.y = (clamp(0, 1, hitColor.y));hitColor.z = (clamp(0, 1, hitColor.z));

任务五:完美反射模型(看到顺便实现了,作业没要求)

核心是重要性采样,参考这篇文章实现的:https://blog.csdn.net/ycrsw/article/details/124408789,各种细节这里面已经讲得非常清楚了,因此就不再赘述,直接给出代码,和Microfacet材质的实现流程差不多,首先添加材质,如下:

enum MaterialType { DIFFUSE, MICROFACET,MIRROR};

main函数中添加物体,赋予材质,

    Material* mirror = new Material(MIRROR, Vector3f(0.0f));mirror->Ks = Vector3f(0.45, 0.45, 0.45);mirror->Kd = Vector3f(0.3, 0.3, 0.25);mirror->ior = 12.85;MeshTriangle floor("../models/cornellbox/floor.obj", white);MeshTriangle shortbox("../models/cornellbox/shortbox.obj", white);MeshTriangle tallbox("../models/cornellbox/tallbox.obj", mirror);MeshTriangle left("../models/cornellbox/left.obj", red);MeshTriangle right("../models/cornellbox/right.obj", green);MeshTriangle light_("../models/cornellbox/light.obj", light);

最后修改eval,在eval中实现完美镜面反射(Mirror),如下:

Vector3f Material::eval(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){switch(m_type){case DIFFUSE:{// calculate the contribution of diffuse modelfloat cosalpha = dotProduct(N, wo);if (cosalpha > 0.0f) {Vector3f diffuse = Kd / M_PI;return diffuse;}elsereturn Vector3f(0.0f);break;}case MIRROR:{float cosalpha = dotProduct(N, wo);if (cosalpha > 0.0f) {float divisor = cosalpha;if (divisor < 0.001) return 0;Vector3f mirror = 1 / divisor;float F;fresnel(wi, N, ior, F);return F * mirror;}elsereturn Vector3f(0.0f);break;}case MICROFACET:{float cosalpha = dotProduct(N, wo);if (cosalpha > 0.0f) {// calculate the contribution of Microfacet modelfloat F, G, D;fresnel(wi, N, ior, F);float Roughness = 1;//超参数,控制粗糙度auto G_function = [&](const float& Roughness, const Vector3f& wi, const Vector3f& wo, const Vector3f& N){float A_wi, A_wo;A_wi = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wi, N))), 2))) / 2;A_wo = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wo, N))), 2))) / 2;float divisor = (1 + A_wi + A_wo);if (divisor < 0.001)return 1.f;elsereturn 1.0f / divisor;};G = G_function(Roughness, -wi, wo, N);auto D_function = [&](const float& Roughness, const Vector3f& h, const Vector3f& N){float cos_sita = dotProduct(h, N);float divisor = (M_PI * pow(1.0 + cos_sita * cos_sita * (Roughness * Roughness - 1), 2));if (divisor < 0.001)return 1.f;else return (Roughness * Roughness) / divisor;};Vector3f h = normalize(-wi + wo);D = D_function(Roughness, h, N);// energy balanceVector3f diffuse = (Vector3f(1.0f) - F) * Kd / M_PI;Vector3f specular;float divisor= ((4 * (dotProduct(N, -wi)) * (dotProduct(N, wo))));if (divisor < 0.001)specular= Vector3f(1);elsespecular = F *G * D / divisor;//std::cout << "F:"<<F << "\n";//std::cout << "diffuse:"<<diffuse<<"\n";//std::cout << "specular:" << specular << "\n";return diffuse+specular;}elsereturn Vector3f(0.0f);break;}}
}

哦对,由于要重要性采样(采样方向只需要采完美反射的方向即可),所以采样的那两个函数也需要改变,不能像Microfacet一样不变。如下:

Vector3f Material::sample(const Vector3f &wi, const Vector3f &N){switch(m_type){case DIFFUSE:{// uniform sample on the hemispherefloat x_1 = get_random_float(), x_2 = get_random_float();float z = std::fabs(1.0f - 2.0f * x_1);float r = std::sqrt(1.0f - z * z), phi = 2 * M_PI * x_2;Vector3f localRay(r*std::cos(phi), r*std::sin(phi), z);return toWorld(localRay, N);break;}case MIRROR:{Vector3f localRay = reflect(wi, N);return localRay;break;}case MICROFACET:{// uniform sample on the hemispherefloat x_1 = get_random_float(), x_2 = get_random_float();float z = std::fabs(1.0f - 2.0f * x_1);float r = std::sqrt(1.0f - z * z), phi = 2 * M_PI * x_2;Vector3f localRay(r * std::cos(phi), r * std::sin(phi), z);return toWorld(localRay, N);break;}}
}float Material::pdf(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){switch(m_type){case DIFFUSE:{// uniform sample probability 1 / (2 * PI)if (dotProduct(wo, N) > 0.0f)return 0.5f / M_PI;elsereturn 0.0f;break;}case MIRROR:{if (dotProduct(wo, N) > 0.0f)return 1.0f;elsereturn 0.0f;break;}case MICROFACET:{// uniform sample probability 1 / (2 * PI)if (dotProduct(wo, N) > 0.0f)return 0.5f / M_PI;elsereturn 0.0f;break;}}
}

哦对了,最后的最后,因为要做重要性采样,castRay函数也要进行修改,否则会有过曝现象,mirror材质不再需要直接光L_dir采样了,就直接算L_indir就行,而且在算L_indir的时候记得那个把里面的避免光源采样的判断条件去了。如下:

Vector3f Scene::castRay(const Ray& ray, int depth) const
{// TO DO Implement Path Tracing Algorithm hereVector3f hitColor = this->backgroundColor;Intersection shade_point_inter = Scene::intersect(ray);if (shade_point_inter.happened){Vector3f p = shade_point_inter.coords;Vector3f wo = ray.direction;Vector3f N = shade_point_inter.normal;Vector3f L_dir(0), L_indir(0);Vector3f p_deviation = (dotProduct(ray.direction, N) < 0) ?p + N * EPSILON :p - N * EPSILON;switch (shade_point_inter.m->getType()){case MIRROR:{//Test Russian Roulette with probability RussianRouolettefloat ksi = get_random_float();if (ksi < RussianRoulette){//wi=sample(wo,N)Vector3f wi = normalize(shade_point_inter.m->sample(wo, N));//Trace a ray r(p,wi)Ray ray_pTowi(p_deviation, wi);//If ray r hit a object at qIntersection bounce_point_inter = Scene::intersect(ray_pTowi);if (bounce_point_inter.happened){float pdf = shade_point_inter.m->pdf(wo, wi, N);if (pdf > EPSILON)L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf * RussianRoulette);}}break;}default:{//sampleLight(inter,pdf_light)Intersection light_point_inter;float pdf_light;sampleLight(light_point_inter, pdf_light);//Get x,ws,NN,emit from interVector3f x = light_point_inter.coords;Vector3f ws = normalize(x - p);Vector3f NN = light_point_inter.normal;Vector3f emit = light_point_inter.emit;float distance_pTox = (x - p).norm();//Shoot a ray from p to xRay ray_pTox(p_deviation, ws);//If the ray is not blocked in the middleffIntersection blocked_point_inter = Scene::intersect(ray_pTox);if (abs(distance_pTox - blocked_point_inter.distance < 0.01)){L_dir = emit * shade_point_inter.m->eval(wo, ws, N) * dotProduct(ws, N) * dotProduct(-ws, NN) / (distance_pTox * distance_pTox * pdf_light);}//Test Russian Roulette with probability RussianRouolettefloat ksi = get_random_float();if (ksi < RussianRoulette){//wi=sample(wo,N)Vector3f wi = normalize(shade_point_inter.m->sample(wo, N));//Trace a ray r(p,wi)Ray ray_pTowi(p_deviation, wi);//If ray r hit a non-emitting object at qIntersection bounce_point_inter = Scene::intersect(ray_pTowi);if (bounce_point_inter.happened && !bounce_point_inter.m->hasEmission()){float pdf = shade_point_inter.m->pdf(wo, wi, N);if (pdf > EPSILON)L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf * RussianRoulette);}}break;}}hitColor = shade_point_inter.m->getEmission()+L_dir + L_indir;hitColor.x = (clamp(0, 1, hitColor.x));hitColor.y = (clamp(0, 1, hitColor.y));hitColor.z = (clamp(0, 1, hitColor.z));}return hitColor;
}

最终结果(均为1024×1024×512)
在这里插入图片描述
这时候有人会问了,tmd,重要性采样怎么这么麻烦,那我不用,把ssp调高点不就行了?emmm,如果头铁,不做重要性采样,我也实验了一下,就是下图的结果,ssp已经是2048了,但是噪点还是非常多,就是采样效率太低了,做了重要性采样后,ssp为512的结果可以完美爆杀这张图。

在这里插入图片描述

代码实现可能遇到的问题

1.光源融入到整个天花板,没有出现黑色

光源融入到整个天花板了,==tmd,感觉网上没有人和我这个情况是一样的。。。

光源尼玛的,就是融入整个天花板了,==tmd,感觉网上没有人和我这个情况是一样的。。。

测试的时候发现,其实这个光源是能被检测出来的,只是着色的时候变成这样了,然后分析了好几个小时的源码,艹,一直找不到原因,最后debug了半天,总算知道为什么了。

就是当对光源着色时,目前是分为两个部分,一个是直接光,一个是间接光。

直接光的话应该是不会提供颜色的,因为直接光同样是在光源上采样,相当于ws和N是几乎垂直的,cos(ws,N)项就会使得整个直接光的值接近0。代码中点偏移的处理只会让两者夹角大于90,此时cos算出来甚至还是小于0的。综上,直接光不会给其提供颜色。

间接光会对其提供颜色,并且也是导致光源颜色和天花板颜色接近的直接原因,因为计算间接光时,光源本身是 漫反射材质,所以这边代码会让光源应该会有light->Kd差不多的颜色,而这个值又和天花板的颜色其实差不多,所以看上去才只能看到天花板。

这个地方我看其他博主的解决方法都很简单粗暴,检测出是光源就直接返回光源本身颜色,但实际上光源也是会吸收反射其他光的,如果场景中有其他光源,那这种处理有可能就会出问题。**因此就需要保证光源是可以正常接收直接光和间接光的,不能直接返回光源本身颜色。**此时观察之前的公式,少了一个自身发光项(下面第二张图,之前一直不知道这个有啥用,此时起作用了,醍醐灌顶),最后把这个加上去就可以正常显示了。

emm,之前好几个小时一直纠结在直接光上面,想方设法要避免直接光,其实并不是这个原因导致的,害==,也是尼玛的运气好,多实验了一下,结果和想的不一样,就再思考分析了下,就想出来原因了。。。

也明白了个道理,发现问题并打算编写代码去解决的时候,先看看能不能设计实验用代码验证一下这个问题的提出是不是正确的,如果这个问题本身就是错误的,那就没有必要花很多时间去写代码解决。我这次就花了很多时间去思考怎么避免直接光,实际上这个问题本身就是不成立的==我应该试一下没有直接光的话,光源区域会不会变黑,如果变黑了,就说明我之前提出的问题是成立的。
在这里插入图片描述
在这里插入图片描述
我草,事后去games论坛上,居然真的找到了个一样的了==
https://games-cn.org/forums/topic/zuoye7wenti/

2.箱子背阴处有很多黑色噪点

没对好伪代码中的那些向量方向定义,说白了就是没完全按照伪代码来,改了之后就好了。
在这里插入图片描述
修改后的结果
在这里插入图片描述

3.有白色噪点(ssp调高仍然有)

画面上有少部分白色噪点
在这里插入图片描述
用ps工具放大,发现是某个像素为白色,此时SPP已经是512,所以应该不是采样不够的问题。而且如果是采样不够,应该是会在其周围同时出现大量噪点,而不是只出现这么一个这么突出的,例如下面第三张图。
在这里插入图片描述
在这里插入图片描述
这意味着计算光照时,出现了一个非常大的值。排查代码发现原因很可能在于除数过小(无限接近于0,或等于0,这里就是等于0),结果接近无穷大。使这个像素的全部采样都收到这个无限大值的影响,直接变成白色。这显然是我们不想要的。这就对应作业里的提示:pdf接近0。解决方案就是pdf低于某个指定阈值时,L_indir为0,代码如下:

float pdf = shade_point_inter.m->pdf(wo, wi, N);
if(pdf> EPSILON)L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf *RussianRoulette);

修改后的结果
在这里插入图片描述
通过这个案例,分析一下为什么要考虑数值精度,emmm,如果不考虑数值精度,很有可能一次数值的不正确就会产生难以想象的结果,例如上述除数为0导致无限大inf的出现,会使得当次像素颜色计算的其他采样结果无效,相当于一个极偏值对整体结果产生了巨大影响,而我们想要的是能够避免极偏值的影响,类似机器学习里面的回归分类问题。所以这个时候就要对其特殊处理,降低他对其他结果的影响。

理论思考

1.偏移为什么要分两种情况这么偏移?

考虑了背光情况(因为世间万物都有厚度,如果光在前面,从背面看就是会被挡住),如下面第二张图。
在这里插入图片描述

2.为什么path tracing有软阴影?

阴影部分应该都是没有直接光的,那么导致软阴影的原因就是环境光,接受的间接光是不一样的,离得近的会被挡住更多的间接光,离得远的间接光挡住的比较少,所以就会有软阴影。

而witted style就没有软阴影,这是因为没有考虑全局光,就是只考虑了那一个方向。

3.把witted style和path tracing的结果对比一下,两者的实现的不同点有哪些?

区别很大,path tracing是基于witted style的改进,很容易看到这两者共同的地方。明显,path tracing是合理的,并且可以基于物理,还能考虑全局光照,能量守恒保证结果更加精确。具体的可以看 Ray Tracing笔记那一章。

反正我觉得两者最大的区别,就是path tracing通过渲染方程考虑了全局光照而witted style无法考虑全局光照(没有全局积分,一个点的颜色就由那么几根光线决定,实现那种完美镜面效果倒是好的很。。。),它因此很难实现很多的细节,大部分材质表现也没有path tracing好。

4.为什么程序中,渲染方程的入射radiance是光源的emit,是一个常量?

emmm,如果要深究,非常复杂,后面的光学理论看得我头痛,大致就下面这么理解就够了。

反正radiance在程序中就是一根光线。
在这里插入图片描述

5.为什么说光栅化比光追快?

光栅化没有大量的采样,光追里面因为要求解积分,需要大量的采样

个人感悟

emmm,可以说这是我从小到大,除一些项目以外,做某门课作业花的最长的时间了,很感谢闫老师的101课程,提供了优质的作业,通过作业7对光线追踪技术有了一个基础的基本认识,也在做的过程中不断发现问题,解决问题,提高自己的coding和思考能力,真的很感谢闫老师,我有一种预感,这门课将会为中国图形学持续输送数以万计的人才。这次作业做完后差不多打好了离线渲染的一些基本的底子,打算明天开始正式做本科的毕设了(之前10月到12月一直在摆烂,1月到2月在补渲染的基础知识,所以说本科毕设正式开始时间就差不多是现在2月,希望来得及==害),加油加油!

也希望这个博客可以对做这个作业的人有一定帮助!

参考资料

1.https://blog.csdn.net/u011942101/article/details/123656944(代码性能分析器)

2.https://blog.csdn.net/ycrsw/article/details/124408789

3.https://blog.csdn.net/qq_41765657/article/details/121942469

4.https://blog.csdn.net/ycrsw/article/details/124565054

5.https://blog.csdn.net/weixin_44491423/article/details/127552276

6.https://games-cn.org/forums/topic/guanyuguangxianzhuizong3zhongradiancedeshizi/

7.https://www.zhihu.com/question/28476602/answer/41003204

8.https://games-cn.org/forums/topic/guanyuxuanranfangchengdiguidingyideyiwen/

9.https://www.jianshu.com/p/0cfc3204af77

10.https://zhuanlan.zhihu.com/p/434964126

11.https://zhuanlan.zhihu.com/p/152226698

相关文章:

GAMES101作业7及课程总结(重点实现多线程加速,微表面模型材质)

目录闲言碎语最终全部效果展示&#xff08;均为10241024512ssp&#xff09;课程总结与理解&#xff08;Path Tracing&#xff09;框架梳理任务一&#xff1a;迁移相关代码任务二&#xff1a;实现path tracing任务三&#xff1a;多线程加速&#xff08;包括其他加速的小trick&am…...

面试题(二十四)数据结构与算法

9.1哈希 请谈一谈&#xff0c;hashCode() 和equals() 方法的重要性体现在什么地方&#xff1f; 考察点&#xff1a;JAVA哈希表 参考回答&#xff1a; Java中的HashMap使用hashCode()和equals()方法来确定键值对的索引&#xff0c;当根据键获取值的时候也会用到这两个方法。…...

【HAL库】STM32CubeMX开发----STM32F407----Uart串口接收空闲中断

一、Uart串口接收空闲中断----详解 首先介绍串口通信的数据传输方式&#xff0c;这样后面的Uart串口空闲中断能更好的理解。 Uart串口通信----数据传输方式 串口通信的数据由发送设备通过自身的TXD接口传输到接收设备得RXD接口。 一个字符一个字符地传输&#xff0c;每个字符…...

Qt_文件操作

本文包含以下内容: 文件操作 基本介绍:ini文件:csv文件:代码功能文件读写:1.1 读取文件1.1.1按行读取1.1.2整体读取1.2 写入文件2. 文件信息读取3. 文件夹的创建4. 获取文件夹下所有的文件5. 获取文件夹及子文件夹下所有的文件用树的方式在界面显示文件夹目录基本介绍: …...

int和Integer有什么区别?

第7讲 | int和Integer有什么区别&#xff1f; Java 虽然号称是面向对象的语言&#xff0c;但是原始数据类型仍然是重要的组成元素&#xff0c;所以在面试中&#xff0c;经常考察原始数据类型和包装类等 Java 语言特性。 今天我要问你的问题是&#xff0c;int 和 Integer 有什么…...

Axure 9 收录不同效果的制作过程

效果类别 一、默认选中实现单选效果 1、默认选中 点击组件&#xff0c;右键选择selected字样&#xff1b; 2、实现单选效果 点击所有组件&#xff0c;右键选择selected group&#xff0c;填好命名&#xff0c;并设置选中时的组件样式&#xff1b;选择其中一个组件&#xf…...

[Datawhale][CS224W]图神经网络(一)

目录一、导读1.1 当前图神经网络的难点1.2 图神经网络应用场景及对应的相关模型&#xff1a;1.3 图神经网络的应用方向及应用场景二、图机器学习、图神经网络编程工具参考文献一、导读 ​ 传统深度学习技术&#xff0c;如循环神经网络和卷积神经网络已经在图像等欧式数据和信号…...

【Android实现16位灰度图数据转RGB数据并以bitmap格式显示】

Android实现16位灰度图数据转RGB数据并以bitmap显示(单通道Gray数据转三通道RGB数据并显示) 需求发现问题解决方案需求 问题需求:项目上需要实现将深度相机传感器给出的数据实时显示出来的功能。经过了解得知,传感器给出的数据为16位灰度图数据,即16位数据表示一个像素的…...

uni-app②

文章目录二、微信小程序简介&#xff08;一&#xff09;文档相关开发者工具使用小程序代码构成小程序基本操作三、uniapp 开发规范uniapp 开发环境开发工具下载 HBuilderX工程搭建项目运行浏览器运行四、组件基础组件基础组件列表组件公共属性集合扩展组件自定义组件UNI-ICON五…...

FFmpeg视频处理

目录 1. Ubuntu&#xff08;wsl&#xff09;安装 ffmpeg 2. ffmpeg查看指令 3. ffmpeg查看媒体文件信息 4. ffmpeg基础操作指令 5. ffmpeg视频抽帧 5.1 基于时间抽取帧 5.2 两种抽帧方式 5.3 视频流抽帧 5.4 视频批量抽帧 6. ffmpeg更改视频播放速度 7. ffmpeg视频格…...

FreeRTOS任务通知 | FreeRTOS十二

目录 说明&#xff1a; 一、任务通知 1.1、什么是任务通知 1.2、任务通知优势与劣势 1.3、任务通知值的更新方式 1.4、任务通知值状态 1.5、任务通知状态 1.6、任务通知方式类型 二、任务通知相关API函数 2.1、常用的发送通知API函数 2.2、带通知值的发送通知函数 …...

CentOS搭建博客typecho

Ubuntu搭建博客typecho_Dyansts的博客-CSDN博客 见过这样的文章展示页面吗&#xff1f; 详细视频安装教程&#xff1a; 9分钟快速搭建typecho博客&#xff0c;让你不再烦恼_哔哩哔哩_bilibili 现在就把他搭建出来 展示页面&#xff1a;Hello World 其他的插件&#xff1a;…...

湖南中创教育PMP如何实施风险应对,避免产生投诉

一、评估风险 评估风险影响的直接或间接价值 面临的潜在威胁&#xff0c;威胁发生的可能性有多大? 威胁一旦发生&#xff0c;损失是多大? 评估承受风险的能力 采取怎样的措施才能将损失降到最低&#xff0c;甚至为零 二、规划风险 对识别出来的风险进行分组或分类 确定…...

Urho3D子系统

通过使用函数RegisterSubsystem()&#xff0c;任何对象都可以作为子系统注册到上下文中。然后&#xff0c;通过调用GetSubsystem()&#xff0c;同一上下文中的任何其他对象都可以访问它们。每个对象类型只能有一个实例作为子系统存在。 发动机初始化后&#xff0c;以下子系统将…...

无线网络术语总结

学习802.11协议&#xff0c;其中有一些英文缩略词&#xff0c;这里做一下总结与记录。 学习资料&#xff1a;知乎徐方鑫 802.11相关文章 802.11协议精读3&#xff1a;CSMA/CD与CSMA/CA - 知乎 (zhihu.com) 无线网络术语缩写全称中文含义APAccessPoint无线访问节点用于无线网络…...

海卡和海派有什么区别

一、海卡和海派有什么区别 海派和海卡实际上就是快船和慢船的区别。都是头程选用海运的方式&#xff0c;海派是到海港海关清关拆柜后&#xff0c;尾程配送是采用快递配送。而海卡则是到海港海关清关拆柜后&#xff0c;尾程选用货车配送。1、海派比较适用于小件货物 海派是海运抵…...

vue3学习资料整理

一、一个后端程序员为什么要学习前端&#xff1f; 1.网上找到的学习理由 《Java后端的我也要学Node.js 了》 https://blog.csdn.net/yusimiao/article/details/104689007 《nodejs后端开发的优缺点&#xff08;nodejs的概念与特征详解&#xff09;》 https://www.1pindao.co…...

Linux基础语法进阶版

Linux基础语法 查看文件内容指令 touch 主要是修改文件时间&#xff0c;多用创建文件 -a #只更改访问时间 -m #只更改修改时间 -c --no-create#不创建任何文件cat 展示小文件内容 -b #对于非空输出行编号 -n #对于所有行输出编号 -E #在每行结束处显示"$" -A #展示所…...

近红外染料标记小分子1628790-37-3,Cyanine5.5 alkyne,花青素CY5.5炔基

试剂基团反应特点&#xff1a;Cyanine5.5 alkyne用于点击化学标记的远红外/近红外染料炔烃。氰基5.5是Cy5.5的类似物&#xff0c;一种流行的荧光团&#xff0c;已广泛用于各种应用&#xff0c;包括完整生物体成像。在温和的铜催化化学条件下&#xff0c;该试剂可与叠氮基共轭&a…...

洛谷——P1004 方格取数

【题目描述】 设有 NN 的方格图 (N≤9)&#xff0c;我们将其中的某些方格中填入正整数&#xff0c;而其他的方格中则放入数字 0。如下图所示&#xff08;见样例&#xff09;: A 0 0 0 0 0 0 0 0 0 0 13 0 0 6 0 0 0 0 0 0 7 0 0 0 0 0 0 14 0 0…...

【Oracle APEX开发小技巧12】

有如下需求&#xff1a; 有一个问题反馈页面&#xff0c;要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据&#xff0c;方便管理员及时处理反馈。 我的方法&#xff1a;直接将逻辑写在SQL中&#xff0c;这样可以直接在页面展示 完整代码&#xff1a; SELECTSF.FE…...

在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:

在 HarmonyOS 应用开发中&#xff0c;手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力&#xff0c;既支持点击、长按、拖拽等基础单一手势的精细控制&#xff0c;也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档&#xff0c…...

java 实现excel文件转pdf | 无水印 | 无限制

文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)

目录 1.TCP的连接管理机制&#xff08;1&#xff09;三次握手①握手过程②对握手过程的理解 &#xff08;2&#xff09;四次挥手&#xff08;3&#xff09;握手和挥手的触发&#xff08;4&#xff09;状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

浅谈不同二分算法的查找情况

二分算法原理比较简单&#xff0c;但是实际的算法模板却有很多&#xff0c;这一切都源于二分查找问题中的复杂情况和二分算法的边界处理&#xff0c;以下是博主对一些二分算法查找的情况分析。 需要说明的是&#xff0c;以下二分算法都是基于有序序列为升序有序的情况&#xf…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

基于Java+VUE+MariaDB实现(Web)仿小米商城

仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意&#xff1a;运行前…...

windows系统MySQL安装文档

概览&#xff1a;本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容&#xff0c;为学习者提供全面的操作指导。关键要点包括&#xff1a; 解压 &#xff1a;下载完成后解压压缩包&#xff0c;得到MySQL 8.…...

libfmt: 现代C++的格式化工具库介绍与酷炫功能

libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库&#xff0c;提供了高效、安全的文本格式化功能&#xff0c;是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全&#xff1a…...