Games101Homework【5】Intersection of a ray with a triangle(Include framework Interpretation)_games101 homework5-程序员宅基地

技术标签: 图形渲染  games101  

Framework analysis:

Since the code is extensive and the author is relatively inexperienced, I've used AI to generate some comments. I can generally understand it. When I have time, I'll explain the entire rendering process, including the formulas and algorithms.

Core:

At the camera origin, shoot a ray towards each pixel (with recursion depth set, the content inside curly braces denotes recursive steps):

{

        If the ray misses, return background color. Then check if the ray intersects with objects in the scene (for spheres: solve the implicit equation of the ray and the 3D sphere, for triangular surfaces: use the Möller–Trumbore algorithm) and save intersection information. Next, based on material information, compute the lighting model (refraction and reflection Fresnel, default Phong model). In cases of refraction and reflection, recursively shoot rays from refraction and reflection points in the direction of refraction and reflection until the maximum depth is reached. Determine the final return color based on the refraction and reflection rates (Fresnel).

}

main.cpp:

The main function demonstrates the entire scene creation process, while rendering occurs in the Renderer class

//主函数中创建场景(创建物体和光源)以及设置渲染选项(图像宽度和高度、最大递归深度、视场角等)。
//然后调用渲染函数进行渲染。
int main()
{
    // 初始化场景,设置窗口大小为1280x960
    Scene scene(1280, 960);
    // 创建并设置第一个球体,位置在(-1, 0, -12),半径为2,材料类型为漫反射和光泽反射,颜色为(0.6, 0.7, 0.8)
    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);
    // 创建并设置第二个球体,位置在(0.5, -0.5, -8),半径为1.5,材料类型为反射和折射,折射率为1.5
    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));    
    // 渲染场景
    Renderer r;
    r.Render(scene);

    return 0;
}

Render.cpp:

Defined ray propagation and rendering algorithms.

#include <fstream>
#include "Vector.hpp"
#include "Renderer.hpp"
#include "Scene.hpp"
#include <optional>

inline float deg2rad(const float &deg)
{ return deg * M_PI/180.0; }

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

// 根据斯涅尔定律计算折射方向的函数
// **功能说明**:
// 此函数主要实现根据斯涅尔定律(Snell's Law)计算光线在穿过不同介质边界时的折射方向。
//斯涅尔定律表明入射角、折射角与两介质的折射率之间的关系。
// **特殊情况处理**:
// 函数需要精细处理两种情况:
// 1. 当光线位于物体外部时:
//    入射角的余弦cosi应基于法向量N和入射向量I的点积计算,但此时需取负值,即 `cosi = -N.I`。
//这是因为通常约定法向量指向物体内部,而光线从外部进入时,二者方向相反。
// 2. 当光线位于物体内部时:
//    需要对以下两点进行调整:
//    a) 交换折射率:计算折射角时,应使用折射率较大的介质作为入射介质,较小的作为出射介质。
//    b) 取反法向量N:确保计算出正确的折射向量,当光线从内部射出时,法向量的方向应指向物体外部,因此需要对其取反。
/**
 @param I 入射光线向量
 * @param N 物体表面的法向量(单位向量)
 * @param ior 光线在两种介质中的折射率比值,即入射介质的折射率除以出射介质的折射率
 * @return 折射后的光线向量。如果光线无法折射(全反射),则返回一个零向量。
**/
Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)
{
      // 计算入射角的余弦值,并确保其在合法范围内
    float cosi = clamp(-1, 1, dotProduct(I, N));
    
    // 初始化介质的折射率
    //etai 和 etat 分别代表光线从一种介质进入另一种介质时的相对折射率。
    //初始化时,假设当前所在介质的折射率为 1(通常指空气或其他参考介质),目标介质的折射率为 ior(指定的折射率)。
    float etai = 1, etat = ior;
    Vector3f n = N; // 初始化法向量

    // 判断光线是从内部射出还是从外部射入,据此调整折射率和法向量的方向
    if (cosi < 0) { cosi = -cosi; } else { std::swap(etai, etat); n = -N; }

    // 计算折射率比值和折射系数
    float eta = etai / etat;
    float k = 1 - eta * eta * (1 - cosi * cosi);

    // 根据折射系数判断是否发生全反射,若发生则返回零向量,否则计算并返回折射向量
    return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}

/**
 * 计算Fresnel方程
 * 
 * 该函数用于计算光线在两种介质交界面处的反射和透射率,根据入射角和材料的折射率来确定。
 * 
 * @param I 入射光线的方向向量
 * @param N 交点处的法线向量
 * @param ior 材料的折射率(指数)
 * 
 * @return 返回一个包含反射和透射率的复数,其中实部表示反射率,虚部表示透射率。
 */
float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)
{
     // 计算入射角的余弦值,并限制在合法范围内
    float cosi = clamp(-1, 1, dotProduct(I, N));
    float etai = 1, etat = ior;
    // 根据入射角的余弦值判断光线是从哪种介质射入另一种介质
    if (cosi > 0) {  std::swap(etai, etat); }
    
    // 使用斯涅尔定律计算折射角的正弦值
    float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
    
    // 判断是否发生全内反射
    if (sint >= 1) {
        return 1; // 全内反射,反射率近似为1
    }
    else {
        // 计算折射后的余弦值
        float cost = sqrtf(std::max(0.f, 1 - sint * sint));
        cosi = fabsf(cosi); // 取入射角余弦值的绝对值
        // 计算反射率 Rs 和 Rp
        float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
        float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
        // 返回反射率和透射率的平均值
        return (Rs * Rs + Rp * Rp) / 2;
    }
    // 由于能量守恒,透射率可以通过反射率计算得到,但该部分代码未在此处显示
    // As a consequence of the conservation of energy, transmittance is given by:
    // kt = 1 - kr;
}

/**
 * 检测光线是否与场景中的物体相交。
 * 
 * \param orig 光线的起点
 * \param dir 光线的方向
 * \param objects 场景中包含的物体列表
 * \param[out] tNear 包含到最近相交物体的距离
 * \param[out] index 如果相交物体是网格,则存储相交三角形的索引
 * \param[out] uv 存储相交点的u和v坐标(对于网格上的纹理映射)
 * \param[out] *hitObject 存储指向相交物体的指针(用于检索材质信息等)
 * \param isShadowRay 是否为阴影光线。如果是,则只要找到一次命中就可以早些返回。
 * 
 * \return 如果光线与物体相交则返回true,否则返回false。
 */
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;
}

/*
 * 实现基于Whitted风格的光线传输算法(E [S*] (D|G) L)
 * 
 * 该函数用于计算由位置和方向定义的光线交点处的颜色。请注意,此函数是递归的(它会调用自身)。
 * 
 * 如果相交对象的材质为反射或反射且折射,则我们计算反射/折射方向,并通过递归调用castRay()函数向场景投射两束新光线。
 * 当表面透明时,我们使用菲涅尔方程的结果混合反射和折射颜色(它根据表面法线、入射视向量和表面折射率计算反射和折射的量)。
 * 
 * 如果表面为漫反射/光泽,则我们使用Phong照明模型来计算交点处的颜色。
 *
 * @param orig 光线的原点
 * @param dir 光线的方向
 * @param scene 场景,包含所有物体和光源的信息
 * @param depth 当前递归的深度
 * @return 在给定点由光线照射的颜色
 */
Vector3f castRay(
        const Vector3f &orig, const Vector3f &dir, const Scene& scene,
        int depth)
{
    // 如果递归深度超过最大深度,返回黑色
    if (depth > scene.maxDepth) {
        return Vector3f(0.0,0.0,0.0);
    }

    Vector3f hitColor = scene.backgroundColor; // 默认颜色,若没有击中任何物体
    if (auto payload = trace(orig, dir, scene.get_objects()); payload)
    {
        Vector3f hitPoint = orig + dir * payload->tNear; // 光线与物体的交点
        Vector3f N; // 法线向量
        Vector2f st; // 纹理坐标
        payload->hit_obj->getSurfaceProperties(hitPoint, dir, payload->index, payload->uv, N, st);
        switch (payload->hit_obj->materialType) {
            case REFLECTION_AND_REFRACTION:
            {
                // 计算反射和折射光线,进行递归光线投射,然后根据菲涅尔方程混合颜色
                Vector3f reflectionDirection = normalize(reflect(dir, N));
                Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));
                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;
                Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
                Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                hitColor = reflectionColor * kr + refractionColor * (1 - kr);
                break;
            }
            case REFLECTION:
            {
                // 计算反射光线并进行递归光线投射,根据菲涅尔反射率混合颜色
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                Vector3f reflectionDirection = reflect(dir, N);
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                             hitPoint + N * scene.epsilon :
                                             hitPoint - N * scene.epsilon;
                hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
                break;
            }
            default:
            {
                // 使用Phong照明模型计算漫反射和镜面反射颜色
                Vector3f lightAmt = 0, specularColor = 0;
                Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
                                           hitPoint + N * scene.epsilon :
                                           hitPoint - N * scene.epsilon;
                // 遍历场景中的所有光源,计算每个光源对交点处颜色的贡献
                for (auto& light : scene.get_lights()) {
                    Vector3f lightDir = light->position - hitPoint;
                    float lightDistance2 = dotProduct(lightDir, lightDir);
                    lightDir = normalize(lightDir);
                    float LdotN = std::max(0.f, dotProduct(lightDir, N));
                    auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
                    bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

                    lightAmt += inShadow ? 0 : light->intensity * LdotN;
                    Vector3f reflectionDirection = reflect(-lightDir, N);

                    specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
                        payload->hit_obj->specularExponent) * light->intensity;
                }

                hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;
                break;
            }
        }
    }

    return hitColor;
}

/*
 * 主渲染函数。
 * 此函数遍历图像中的所有像素,生成主光线,并将这些光线投射到场景中。
 * 帧缓冲区的内容将被保存到一个文件中。
 */
void Renderer::Render(const Scene& scene)
{
    std::vector<Vector3f> framebuffer(scene.width * scene.height);
    // 计算视野调整系数和图像宽高比
    float scale = std::tan(deg2rad(scene.fov * 0.5f));
    float imageAspectRatio = scene.width / (float)scene.height;

    // Use this variable as the eye position to start your rays.
    Vector3f eye_pos(0);
    int m = 0;
    for (int j = 0; j < scene.height; ++j)
    {
        for (int i = 0; i < scene.width; ++i)
        {
            // 生成主光线方向
            float x = (2*((float)i+0.5)/scene.width-1.0f) * imageAspectRatio * scale;
            float y = (1.0-2*((float)j+0.5)/scene.height) * scale;
         

            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);
    }

    // 将帧缓冲区内容保存到文件中
    FILE* 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] = (char)(255 * clamp(0, 1, framebuffer[i].x));
        color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
        color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
        fwrite(color, 1, 3, fp);
    }
    fclose(fp);    
}

Triangle.hpp :  

Defined operations for configuring triangles, ray-triangle intersection, obtaining vertex information, and more.

#pragma once

#include "Object.hpp"

#include <cstring>
/**
 * 检测一条射线和一个三角形是否相交。
 * 
 * @param v0 三角形的第一个顶点。
 * @param v1 三角形的第二个顶点。
 * @param v2 三角形的第三个顶点。
 * @param orig 射线的原点。
 * @param dir 射线的方向(单位向量)。
 * @param tnear 如果相交,存储最近的交点距离(从射线原点开始)。
 * @param u 在三角形上的参数值,表示交点相对于 v0 的位置。
 * @param v 在三角形上的参数值,表示交点相对于 v1 的位置。
 * @return 如果射线和三角形相交,返回 true;否则返回 false。
 */
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);

    // 计算 s1 和 e1 的点积,用于后续交点参数的计算
    float s1e1 = dotProduct(s1,e1);
    if (dotProduct(s1,e1)<=0) return false; // 如果 s1 和 e1 的点积小于等于 0,则射线方向与三角形法向量方向相反,不相交

    // 根据向量的点积计算交点参数 tnear、u 和 v
    tnear=dotProduct(s2,e2)/s1e1;
    u=dotProduct(s1,s)/s1e1;
    v=dotProduct(s2,dir)/s1e1;

    // 检查交点参数是否满足三角形的边界条件,如果满足则表示相交
    if(tnear>=0.0f&&u>=0.0f&&v>=0.0f&&(1.0f-u-v)>=0.0f){
        return true;
    }
    return false;   

}

class MeshTriangle : public Object
{
public:
/**
 * 构造一个MeshTriangle对象。
 * 
 * @param verts 指向包含顶点位置数据的Vector3f数组的指针。
 * @param vertsIndex 指向包含顶点索引数据的uint32_t数组的指针,用于定义三角形。
 * @param numTris 三角形的数量。
 * @param st 指向包含UV纹理坐标数据的Vector2f数组的指针。
 * 
 * 此构造函数通过给定的顶点位置、顶点索引、三角形数量和UV纹理坐标初始化MeshTriangle对象。
 * 它会计算出最大的顶点索引以确定需要分配多少个顶点和UV坐标。
 * 然后,它会复制顶点位置、顶点索引和UV坐标到内部持有的数组中以供后续使用。
 */
    MeshTriangle(const Vector3f* verts, const uint32_t* vertsIndex, const uint32_t& numTris, const Vector2f* st)
    {
        // 计算顶点索引中的最大值,以确定需要为顶点和UV坐标分配多少空间。
    uint32_t maxIndex = 0;
    for (uint32_t i = 0; i < numTris * 3; ++i)
        if (vertsIndex[i] > maxIndex)
            maxIndex = vertsIndex[i];
    maxIndex += 1;

    // 分配内存以存储顶点位置,并复制数据。
    vertices = std::unique_ptr<Vector3f[]>(new Vector3f[maxIndex]);
    memcpy(vertices.get(), verts, sizeof(Vector3f) * maxIndex);

    // 分配内存以存储顶点索引,并复制数据。
    vertexIndex = std::unique_ptr<uint32_t[]>(new uint32_t[numTris * 3]);
    memcpy(vertexIndex.get(), vertsIndex, sizeof(uint32_t) * numTris * 3);

    // 存储三角形的数量。
    numTriangles = numTris;

    // 分配内存以存储UV纹理坐标,并复制数据。
    stCoordinates = std::unique_ptr<Vector2f[]>(new Vector2f[maxIndex]);
    memcpy(stCoordinates.get(), st, sizeof(Vector2f) * maxIndex);
    }

 /**
 * 检测一条射线与一组三角形的交点。
 * 
 * @param orig 射线的原点
 * @param dir 射线的方向(单位向量)
 * @param tnear 返回最近的交点距离(射线原点到交点的距离)
 * @param index 返回与射线相交的三角形的索引
 * @param uv 在相交的三角形上的UV坐标
 * @return 如果射线与三角形组相交返回true,否则返回false
 * 
 * 该函数遍历一组三角形,使用rayTriangleIntersect函数检测每个三角形与射线的交点。
 * 如果检测到交点且该交点比当前最接近的交点更近,则更新tnear、uv和index。
 */
    bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t& index,
                   Vector2f& uv) const override
    {
        bool intersect = false; // 初始时假定不相交
        for (uint32_t k = 0; k < numTriangles; ++k) // 遍历所有三角形
        {
            // 获取当前三角形的三个顶点
            const Vector3f& v0 = vertices[vertexIndex[k * 3]];
            const Vector3f& v1 = vertices[vertexIndex[k * 3 + 1]];
            const Vector3f& v2 = vertices[vertexIndex[k * 3 + 2]];
            
            float t, u, v; // 临时变量用于存储交点信息
            
            // 检测当前三角形与射线是否相交,并更新最近交点信息
            if (rayTriangleIntersect(v0, v1, v2, orig, dir, t, u, v) && t < tnear)
            {
                tnear = t;
                uv.x = u;
                uv.y = v;
                index = k;
                intersect |= true; // 更新相交状态
            }
        }

        return intersect; // 返回是否相交的结果
    }

   /**
     * 获取给定点在表面的属性,包括法向量和纹理坐标。
     * 
     * @param p1 三角形的第一个顶点坐标
     * @param p2 三角形的第二个顶点坐标
     * @param p3 三角形的第三个顶点坐标
     * @param index 三角形的索引
     * @param uv 在三角形内的纹理坐标
     * @param N 输出的法向量
     * @param st 输出的纹理坐标
     */
    void getSurfaceProperties(const Vector3f&, const Vector3f&, const uint32_t& index, const Vector2f& uv, Vector3f& N,
                              Vector2f& st) const override
    {
        // 根据索引获取三角形的三个顶点
        const Vector3f& v0 = vertices[vertexIndex[index * 3]];
        const Vector3f& v1 = vertices[vertexIndex[index * 3 + 1]];
        const Vector3f& v2 = vertices[vertexIndex[index * 3 + 2]];

        // 计算两个边向量,并标准化
        Vector3f e0 = normalize(v1 - v0);
        Vector3f e1 = normalize(v2 - v1);

        // 计算并标准化法向量
        N = normalize(crossProduct(e0, e1));

        // 获取三个顶点的纹理坐标
        const Vector2f& st0 = stCoordinates[vertexIndex[index * 3]];
        const Vector2f& st1 = stCoordinates[vertexIndex[index * 3 + 1]];
        const Vector2f& st2 = stCoordinates[vertexIndex[index * 3 + 2]];

        // 根据给定的纹理坐标uv,计算最终的纹理坐标
        st = st0 * (1 - uv.x - uv.y) + st1 * uv.x + st2 * uv.y;
    }

     /**
     * 计算并返回漫反射颜色。
     * @param st 二维纹理坐标,用于评估漫反射颜色。
     * @return Vector3f 表示漫反射颜色的三维向量。
     */
    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);
    }

    std::unique_ptr<Vector3f[]> vertices;
    uint32_t numTriangles;
    std::unique_ptr<uint32_t[]> vertexIndex;
    std::unique_ptr<Vector2f[]> stCoordinates;
};

Principles:

How to calculate rays and obtain color information of triangles on the rays?

Calculate the position of a pixel in world coordinates, then cast a ray from the camera position through the pixel position. Upon hitting an object, return the color value and store it in the buffer.

Because I wasn't previously familiar with how to calculate the position of a pixel in world coordinates, I referred to the following article:

Games101:作业5解析_games101作业5-程序员宅基地

使用光线追踪生成相机光线 (scratchapixel.com)j

explan:

Calculating the position of a pixel in world coordinates:

Raster space:

The coordinates are (i+0.5, j+0.5)

NDC space (mapped to 0 to 1):

Raster space x-coordinate / window width, Raster space y-coordinate / window height.

Screen space (mapping from 0 to 1 to -1 to 1):

For x-coordinate:

(2 * NDC space x-coordinate) - 1

But because the original NDC space y-coordinate is opposite to the screen space, for the y-coordinate:

1 - (2 * NDC space y-coordinate)

Because mapping to -1 to 1 causes compression, for example, if x:y is compressed from 1.4:1 to 1:1, you need to calculate the aspect ratio to scale x back up (if y is larger than x, you only need to multiply x to maintain the correct aspect ratio).

Finally, calculate tan(fov/2) to obtain the ratio of screen height to the distance from the camera to the screen as the scaling factor.

The resulting expression is as follows:

 float imageAspectRatio = scene.width / (float)scene.height;

 float scale = std::tan(deg2rad(scene.fov * 0.5f));   

float x = (2*((float)i+0.5)/scene.width-1.0f) * imageAspectRatio * scale;
float y = (1.0-2*((float)j+0.5)/scene.height) * scale; 

Moller-Trumbore:

Simply combine the ray equation and the barycentric representation of the triangle's points:

一文读懂射线与三角形相交算法Moller-Trumbore算法【收藏好文】 - 知乎 (zhihu.com)

Just implement the formulas.

Code:

Render():

/*
 * 主渲染函数。
 * 此函数遍历图像中的所有像素,生成主光线,并将这些光线投射到场景中。
 * 帧缓冲区的内容将被保存到一个文件中。
 */
void Renderer::Render(const Scene& scene)
{
    std::vector<Vector3f> framebuffer(scene.width * scene.height);
    // 计算视野调整系数和图像宽高比
    float scale = std::tan(deg2rad(scene.fov * 0.5f));
    float imageAspectRatio = scene.width / (float)scene.height;

    // Use this variable as the eye position to start your rays.
    Vector3f eye_pos(0);
    int m = 0;
    for (int j = 0; j < scene.height; ++j)
    {
        for (int i = 0; i < scene.width; ++i)
        {
            // 生成主光线方向
            float x = (2*((float)i+0.5)/scene.width-1.0f) * imageAspectRatio * scale;
            float y = (1.0-2*((float)j+0.5)/scene.height) * scale;
         

            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);
    }

    // 将帧缓冲区内容保存到文件中
    FILE* 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] = (char)(255 * clamp(0, 1, framebuffer[i].x));
        color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
        color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
        fwrite(color, 1, 3, fp);
    }
    fclose(fp);    
}

Note: If strange images appear, check whether Vector3f dir = normalize(Vector3f(x, y, -1)); is normalized.

 rayTriangleIntersect():

/**
 * 检测一条射线和一个三角形是否相交。
 * 
 * @param v0 三角形的第一个顶点。
 * @param v1 三角形的第二个顶点。
 * @param v2 三角形的第三个顶点。
 * @param orig 射线的原点。
 * @param dir 射线的方向(单位向量)。
 * @param tnear 如果相交,存储最近的交点距离(从射线原点开始)。
 * @param u 在三角形上的参数值,表示交点相对于 v0 的位置。
 * @param v 在三角形上的参数值,表示交点相对于 v1 的位置。
 * @return 如果射线和三角形相交,返回 true;否则返回 false。
 */
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);

    // 计算 s1 和 e1 的点积,用于后续交点参数的计算
    float s1e1 = dotProduct(s1,e1);
    if (dotProduct(s1,e1)<=0) return false; // 如果 s1 和 e1 的点积小于等于 0,则射线方向与三角形法向量方向相反,不相交

    // 根据向量的点积计算交点参数 tnear、u 和 v
    tnear=dotProduct(s2,e2)/s1e1;
    u=dotProduct(s1,s)/s1e1;
    v=dotProduct(s2,dir)/s1e1;

    // 检查交点参数是否满足三角形的边界条件,如果满足则表示相交
    if(tnear>=0.0f&&u>=0.0f&&v>=0.0f&&(1.0f-u-v)>=0.0f){
        return true;
    }
    return false;   

}

Note: If strange images appear, check whether uv values are assigned.

Result:

cool! 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_61943345/article/details/137149599

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文