首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

games101 作业4及作业5 详解光线追踪框架

编程知识
2024年08月16日 15:56

games101 作业4及作业5 详解光线追踪框架

作业4

代码分析

作业四的代码整体比较简单 主要流程就是 通过鼠标事件 获取四个控制点的坐标 然后绘制贝塞尔曲线的内容就由我们来完成

理论分析

贝塞尔曲线的理论就是给定一组控制点 然后不断的在控制点之间进行插值 再在得到的新的插值点之间进行插值 具体过程可以用树形结构来表示:
img

我们可以使用递归来推导插值得到贝塞尔曲线上的点的位置 也可以使用二项分布多项式来表示:
img

贝塞尔曲线的优良性质:
img
1.曲线的起点与终点一定是控制点的第一个点与最后一个点
2.曲线在起点和终点处分别会与第一个和最后一个控制点之间的线段保持切线一致
3.可以通过变换控制点来变换整条曲线,无需重新计算曲线的所有点
4.曲线完全包含在由控制点构成的凸包内部。凸包是指能够完全包围所有控制点的最小凸多边形。因此,贝塞尔曲线不会离开这个多边形的范围。这个特性帮助我们确保曲线的形状在控制点之间得到良好的控制

贝塞尔曲线的抗锯齿 锯齿的来源就是曲线和背景像素过渡的不自然 我们对像素周边的其它四个像素 根据距离 给背景像素一个平均的颜色即可

贝塞尔曲面 就是对一组贝塞尔曲线进行进一步的插值:
img

实际解决

这里我尽量不改变原来的代码结构 在recursive_bezier中进行递归 相当于每次递归都算出树形结构中插值得到的一层点 直至递归到插值得到的只有一个点返回

cv::Vec3b blendColors(const cv::Vec3b& color1, const cv::Vec3b& color2, float alpha)
{
    return color1 * (1.0 - alpha) + color2 * alpha;
}

void draw_anti_aliased_pixel(cv::Mat& window, cv::Point2f point, cv::Vec3b color)
{
    int x = static_cast<int>(std::floor(point.x));
    int y = static_cast<int>(std::floor(point.y));

    float alpha_x = point.x - x;
    float alpha_y = point.y - y;

    // Blend colors based on distance to pixel center
    window.at<cv::Vec3b>(y, x) = blendColors(window.at<cv::Vec3b>(y, x), color, (1 - alpha_x) * (1 - alpha_y));
    window.at<cv::Vec3b>(y, x + 1) = blendColors(window.at<cv::Vec3b>(y, x + 1), color, alpha_x * (1 - alpha_y));
    window.at<cv::Vec3b>(y + 1, x) = blendColors(window.at<cv::Vec3b>(y + 1, x), color, (1 - alpha_x) * alpha_y);
    window.at<cv::Vec3b>(y + 1, x + 1) = blendColors(window.at<cv::Vec3b>(y + 1, x + 1), color, alpha_x * alpha_y);
}

cv::Point2f recursive_bezier(const std::vector<cv::Point2f>& control_points, float t)
{
    cv::Point2f point;
    int size = control_points.size();
    std::vector<cv::Point2f> new_points(size);
    if (size == 1) {
        return control_points[0];
    }
    // TODO: Implement de Casteljau's algorithm
    for (int i = 0; i < size - 1; i++)
    {
        new_points[i] = (1 - t) * control_points[i] + t * control_points[i + 1];
    }
    new_points.resize(size - 1);
    point = recursive_bezier(new_points, t);
    return point;

}
void bezier(const std::vector<cv::Point2f>& control_points, cv::Mat &window)
{
    cv::Vec3b color(0, 255, 0); // 绿色
    for (double t = 0.0; t <= 1.0; t += 0.001)
    {
        cv::Point2f point = recursive_bezier(control_points, t);
        //window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
        draw_anti_aliased_pixel(window, point, color);
    }

}

结果展示:

img

img

作业5

代码分析

整体的代码框架大致如下:
初始化场景 场景中包含物体 灯光等等
场景中会包含整个rast space的大小 fov 相机的视角宽度 背景颜色
maxDepth用于控制光线发生反射折射的次数 我们不能让光线无限的传播下去
epsilon 用于防止浮点数精度问题 导致计算和物体交点时计算到了表面的下方 这样再次反射会又一次和相同的表面相交

class Scene
{
public:
    // setting up options
    int width = 1280;
    int height = 960;
    double fov = 90;
    Vector3f backgroundColor = Vector3f(0.235294, 0.67451, 0.843137);
    int maxDepth = 5;
    float epsilon = 0.00001;

    Scene(int w, int h) : width(w), height(h)
    {}

    void Add(std::unique_ptr<Object> object) { objects.push_back(std::move(object)); }
    void Add(std::unique_ptr<Light> light) { lights.push_back(std::move(light)); }

    [[nodiscard]] const std::vector<std::unique_ptr<Object> >& get_objects() const { return objects; }
    [[nodiscard]] const std::vector<std::unique_ptr<Light> >&  get_lights() const { return lights; }

private:
    // creating the scene (adding objects and lights)
    std::vector<std::unique_ptr<Object> > objects;
    std::vector<std::unique_ptr<Light> > lights;
};

框架中用到了两种物体 分别是两个球体 和 三角形网格 这里的三角形网格是两个三角形 拼接成的正方形 两个球体的材质一个是glossy_specular 一个是反射透射材质 三角形网格是glossy_specular材质:

auto sph1 = std::make_unique<Sphere>(Vector3f(-1, 0, -12), 2);
sph1->materialType = DIFFUSE_AND_GLOSSY;
sph1->diffuseColor = Vector3f(0.6, 0.7, 0.8);

auto sph2 = std::make_unique<Sphere>(Vector3f(0.5, -0.5, -8), 1.5);
sph2->ior = 1.5;
sph2->materialType = REFLECTION_AND_REFRACTION;

scene.Add(std::move(sph1));
scene.Add(std::move(sph2));

Vector3f verts[4] = {{-5,-3,-6}, {5,-3,-6}, {5,-3,-16}, {-5,-3,-16}};
uint32_t vertIndex[6] = {0, 1, 3, 1, 2, 3};
Vector2f st[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
auto mesh = std::make_unique<MeshTriangle>(verts, vertIndex, 2, st);
mesh->materialType = DIFFUSE_AND_GLOSSY;

scene.Add(std::move(mesh));
scene.Add(std::make_unique<Light>(Vector3f(-20, 70, 20), 0.5));
scene.Add(std::make_unique<Light>(Vector3f(30, 50, -12), 0.5));    

之后就是投射camera ray 计算每个像素的颜色
trace用于计算和物体的交点 球体的话就用解析解 三角形网格需要遍历每个三角形图元求交点 使用Moller-Trumbore 算法来计算交点 注意这里求得交点之后还要判断是不是最近的:

std::optional<hit_payload> trace(
        const Vector3f &orig, const Vector3f &dir,
        const std::vector<std::unique_ptr<Object> > &objects)
{
    float tNear = kInfinity;
    std::optional<hit_payload> payload;
    for (const auto & object : objects)
    {
        float tNearK = kInfinity;
        uint32_t indexK;
        Vector2f uvK;
        if (object->intersect(orig, dir, tNearK, indexK, uvK) && tNearK < tNear)
        {
            payload.emplace();
            payload->hit_obj = object.get();
            payload->tNear = tNearK;
            payload->index = indexK;
            payload->uv = uvK;
            tNear = tNearK;
        }
    }

    return payload;
}

castray中 反射折射材质如果光线弹射的次数的超过我们设定的五次 就会终止递归 或者打到了diffuse_glossy材质也会终止
diffuse_glossy材质的着色计算就采用bling-phong模型
这里代码很多 就不贴了 讲几个细节
1.反射计算:
img

Vector3f reflect(const Vector3f &I, const Vector3f &N)
{
    return I - 2 * dotProduct(I, N) * N;
}

简单的向量运算
2.折射计算
这里的推导见:https://www.cnblogs.com/night-ride-depart/p/7429618.html
虽然他整体推导有些复杂 但是思路还是对的 我也推了一下没啥问题 就偷个懒
img
这里作业加入了关于法线 与 入射光线是否在同侧的讨论
如果同侧 点积小于0 说明是从物体的外部打来的光线
如果异侧 点积大于0 说明是从物体的内部打来的光线 这是需要调整我们表面法线的方向,并且光密到光疏也要相应的调整:

Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)
{
    float cosi = clamp(-1, 1, dotProduct(I, N));
    float etai = 1, etat = ior;
    Vector3f n = N;
    //根据法线和入射光的位置 进行相应的调整
    if (cosi < 0) { cosi = -cosi; ???} else { std::swap(etai, etat); n= -N; }
    float eta = etai / etat;
    //全反射临界计算 计算cos’
    float k = 1 - eta * eta * (1 - cosi * cosi);
    return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}

这里我觉得是不是框架的代码有点问题 其实点积算cos都是负的 为什么在小于0的时候再取负 这样下面eta * cosi - sqrtf(k)又要反过来 所以其实cosi = -cosi;这句是不需要的?

3.epsilon的使用
我们之前提到epsilon 用于控制hitpoint位置变化 防止再次打到相同表面 这里也要根据同侧还是异侧进行一个调整
如果是 反射 同侧就是加 异侧应该是减 如果是折射 同侧就是减 异侧应该是加 所以这里框架写的是不是有点问题:

Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                           + ? hitPoint - N * scene.epsilon :
                            -? hitPoint + N * scene.epsilon;
Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
                             hitPoint - N * scene.epsilon :
                             hitPoint + N * scene.epsilon;

4.shadow的生成
从hitpoint处向光源打一根光线 检测是否打到物体 并且检测物体是不是在光源与hitpoint之间(用距离判断)同时满足 则该点为阴影:

Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
                           hitPoint + N * scene.epsilon :
                           hitPoint - N * scene.epsilon;
auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

结果展示:
img

这里下面那个棋盘的正方形 就是一开始初始化的三角形网格 可以看到图中有阴影 也能看出一些折射 反射的现象 后面那个球就是diffuse_glossy材质 上面也能看出一些高光
棋盘格纹理的生成:

 Vector3f evalDiffuseColor(const Vector2f& st) const override
 {
     float scale = 5;
     float pattern = (fmodf(st.x * scale, 1) > 0.5) ^ (fmodf(st.y * scale, 1) > 0.5);
     return lerp(Vector3f(0.815, 0.235, 0.031), Vector3f(0.937, 0.937, 0.231), pattern);
 }

理论分析

本次作业要完成的内容比较简单
首先是生成camera ray 需要我们将rast space上的二维点转换成三维点 需要经历一系列变换 可参考我之前的一篇博文:https://www.cnblogs.com/dyccyber/p/17806284.html 最后乘以 * imageAspectRatio * scale 其实就是转换到世界空间 对应着我们作业1透视矩阵对xy坐标的缩放

然后是计算三角形与光线的相交 就是套用课上的公式 需要注意的是要加入边界范围的限制 一个是确定不是光线的反向相交 即tnear>0 一个是重心坐标在0-1之间 防止交点在三角形外部

实际解决

for (int j = 0; j < scene.height; ++j)
{
    for (int i = 0; i < scene.width; ++i)
    {
        // generate primary ray direction
        float x = (2 * ((i + 0.5) / (float)scene.width) - 1) * imageAspectRatio * scale;
        float y = (1 - 2 * ((j + 0.5) / (float)scene.height)) * scale;
        // TODO: Find the x and y positions of the current pixel to get the direction
        // vector that passes through it.
        // Also, don't forget to multiply both of them with the variable *scale*, and
        // x (horizontal) variable with the *imageAspectRatio*            

        Vector3f dir = normalize(Vector3f(x, y, -1)); // Don't forget to normalize this direction!
        framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
    }
    UpdateProgress(j / (float)scene.height);
}
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
                          const Vector3f& dir, float& tnear, float& u, float& v)
{
    // TODO: Implement this function that tests whether the triangle
    // that's specified bt v0, v1 and v2 intersects with the ray (whose
    // origin is *orig* and direction is *dir*)
    // Also don't forget to update tnear, u and v.

    Vector3f E1 = v1 - v0;
    Vector3f E2 = v2 - v0;
    Vector3f S = orig - v0;
    Vector3f S1 = crossProduct(dir, E2);
    Vector3f S2 = crossProduct(S, E1);
    float div = dotProduct(S1,E1);
    tnear = dotProduct(S2, E2) / div;
    u = dotProduct(S1, S) / div;
    v = dotProduct(S2, dir) / div;
    //两个边界 一个是光线的方向 一个是交点要在三角形内部
    if (tnear >= 0 && u >=0 && u<=1 && v>=0 && v<=1) {
        return true;
    }
    return false;
}
From:https://www.cnblogs.com/dyccyber/p/18363220
本文地址: http://shuzixingkong.net/article/1163
0评论
提交 加载更多评论
其他文章 .NET8 Blazor 从入门到精通:(二)组件
目录Blazor 组件基础路由导航参数组件参数路由参数生命周期事件状态更改组件事件 Blazor 组件 基础 新建一个项目命名为 MyComponents ,项目模板的交互类型选 Auto ,其它保持默认选项: 客户端组件 (Auto/WebAssembly): 最终解决方案里面会有两个项目:服务器
.NET8 Blazor 从入门到精通:(二)组件 .NET8 Blazor 从入门到精通:(二)组件 .NET8 Blazor 从入门到精通:(二)组件
别再被坑了! JavaScript类型检测的最佳实践
别再被坑了! JavaScript类型检测的最佳实践 在 JavaScript 中,我们经常需要判断一个变量的类型。这个需求在编程中非常常见,因为不同类型的数据会影响到我们的代码逻辑。 JavaScript 提供了几种方法来检测数据类型,每种方法都有自己的优缺点。 Object.prototype.
别再被坑了! JavaScript类型检测的最佳实践
mysql8.0 主从架构模式【0到1架构系列】
前提条件 准备3,4,5台虚拟机 祼装mysql8.0 主从架构 常见两种模式“一主多从”和“级联复制”两种,基本都很简单,都是依赖binlog日志文件进行同步,binlog日志会记录DDL和部分DDL语句,进行同步时从库会重新执行这些语句从而实现主从同步。 步骤1: 配置主/从服务器的server
mysql8.0 主从架构模式【0到1架构系列】 mysql8.0 主从架构模式【0到1架构系列】 mysql8.0 主从架构模式【0到1架构系列】
reduce() 多种用法
reduce()方法用于将数组简化为单一值,通过遍历数组并应用提供的函数。它可以用于求和、乘积、计算对象属性的总和、数组去重和转换数组结构等。初始值的设置会影响reduce的起始索引。不提供初始值时,从索引1开始执行;提供初始值则从索引0开始。 一、 定义和用法reduce() 方法将数组缩减为单个
WPF 怎么把checkbox改成开关样式
先看一下效果吧: isChecked = false 的时候的效果 isChecked = true 的时候的效果 然后我们来实现一下这个效果吧 第一步:创建一个空的wpf项目; 第二步:在项目里面添加一个checkbox &lt;Grid&gt; &lt;CheckBox HorizontalAl
WPF 怎么把checkbox改成开关样式 WPF 怎么把checkbox改成开关样式 WPF 怎么把checkbox改成开关样式
CRC算法原理、推导及实现
CRC, Cyclic Redundancy Check, 循环冗余校验 1. 基本原理 CRC的本质是除法,把待检验的数据当作一个很大(很长)的被除数,两边选定一个除数(有的文献叫poly),最后得到的余数就是CRC的校验值。 判定方法: 将消息和校验和分开。计算消息的校验和(在附加W个零后),并
树莓派CM4(三): 定制自己的树莓派镜像
1. 镜像下载 使用树莓派最新的镜像Raspberry Pi OS Lite,内核版本6.6 下载链接 https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-07-04/2024-07-
树莓派CM4(三): 定制自己的树莓派镜像 树莓派CM4(三): 定制自己的树莓派镜像 树莓派CM4(三): 定制自己的树莓派镜像
unity游戏源码和教程:智能分析话语的三维唯美世界
unity游戏源码和教程:智能分析话语的三维唯美世界。 这个游戏的源码(含教程文档)我放到了夸克网盘https://pan.quark.cn/s/618fb9459029 话语分析是有用的,假如游戏中,你是队长,带着NPC队友张三和李四,路上遇到蛇,你可以说“张三打蛇,李四保护张三。”这就需要先分析
unity游戏源码和教程:智能分析话语的三维唯美世界 unity游戏源码和教程:智能分析话语的三维唯美世界 unity游戏源码和教程:智能分析话语的三维唯美世界