【计算机图形学】OpenGL递归实现光线追踪

这篇具有很好参考价值的文章主要介绍了【计算机图形学】OpenGL递归实现光线追踪。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

计算机图形学课程设计:基于面向对象的光线跟踪算法设计与实现

目录

一、前言

二、项目实现与说明

1. 数据结构设计

1.1 光线 Ray

1.2 材质 Material

1.3 光照 Light

1.4 相机 Camera

1.5 球体Sphere

1.6 场景Scene

2. 算法实现

2.1 光线追踪算法原理与步骤

2.2 计算观察光线

2.3 光线与物体(球体)相交

2.4 光线追踪算法的实现

2.4.1 遮挡剔除

2.4.2 阴影

2.4.3 镜面反射

2.4.4 折射

 2.4.5 包含递归调用的光线追踪函数

2.5 其他

三、总结


一、前言

这个项目是数字媒体技术专业必修课《计算机图形学》的课程设计之一,当时大二学这门课的时候由于比较菜没有写出来,现在大三比较空了就抽时间(摸鱼)重新写了一下,花了一个星期看了一堆大佬的文章,缝合了一堆大佬的代码,总算是能够实现了。由于我依旧是这么的菜,如果写的内容出现问题了也请见谅;

下面对课程设计要求、环境配置方法、最终实现效果等进行说明,直接goto代码可以直接跳过: 

注:项目参考了计算机图形学教材《计算机图形学》Peter Shirley著(第2版)和一些老师给的可课件、代码和伪代码;

实验内容:基于C++(也可选择其它编程语言,但需要在实现中体现面向对象的思想)实现完整的含递归调用的光线跟踪算法;

场景要求:至少包含两个球体;

实验效果:要求实现Phong局部光照明、物体之间的镜面反射、阴影这三种效果;如果想拿更好的成绩,可以加入折射的效果;

实验环境:Macbook Pro 2019, intel i9 处理器;系统:macOS Monterey 12.5;使用IDE: Xcode v13.4.1;

最终效果图

(如图所示,场景中包含6个球体,分别为红色、黄色、蓝色的3个粗糙材质球体,上方的反射材质球体,右前方的一个折射材料球体以及下方充当场景地面的反射材质大球体,满足实验场景要求)

【计算机图形学】OpenGL递归实现光线追踪
图1 最终效果图

 OpenGL环境配置

1. Xcode配置OpenGL开发环境:

MAC下的XCode配置OpenGL环境(GLFW、GLAD)_Herculeser的博客-CSDN博客

2. 下载第三方库glm:

为了方便之后的向量运算,需要使用glm(OpenGl Mathematics)库;

github地址:https://github.com/g-truc/glm,下载解压后拖进项目路径,然后在在项目设置页面中的Search Paths->User Header Search Paths中添加glm文件夹的路径,使用时只需#include "glm.hpp" 即可。

项目地址

项目地址 (完整项目,运行结果与上图一致)

二、项目实现与说明

本实验要求实现含有完整的递归调用的光线追踪算法,由于GLSL不支持递归调用,故使用C++在CPU中运行包含递归调用的光线追踪算法,得出每一个像素的颜色值进行绘制;

1. 数据结构设计

包括光线、材质、物体(球体)、相机、场景等,以便实现光线追踪算法的运算;

1.1 光线 Ray

光线是光线追踪算法中最基本的数据结构,在光线追踪算法中,我们追踪从相机射向场景上每一个像素点的光线,检测光线与场景中物体的碰撞、反射、折射,以计算出该像素点的颜色。

根据书本上光线的表达式:【计算机图形学】OpenGL递归实现光线追踪,取向量d = s - e,即光线的方向,则可以将任意光线表达为由光源e出发沿着方向d前进的射线,表达式为【计算机图形学】OpenGL递归实现光线追踪,故在项目中定义光线结构体 Ray如下:

struct Ray{
    glm::vec3 ori;   //光线源点
    glm::vec3 dir;   //光线方向
    
    Ray(glm::vec3 ori, glm::vec3 dir) : ori(ori), dir(dir) {}    //光线结构体初始化
};

1.2 材质 Material

在本项目的实验要求中,要求实现反射、折射效果,故定义材质这一数据结构以存储场景中物体的材质类型以及相关参数。

材质分为3种类型,即仅进行Phong光照模型计算的粗糙材质、进行反射计算的反射材质和进行反射与折射计算的折射材质,分别由定义的宏ROUGH, REFLECTIVE, REFRACTIVE表示;

粗糙类型的材质的参数有用于Phong模型计算的物体颜色objColor与高光指数shiness;全反射材质的参数有反射系数R0;折射材质的参数有折射率n;三种类型的材质分别有对应的构造函数重载。项目中材质结构体Material定义如下:

struct Material{
    int type;
    
    //Phong模型参数
    glm::vec3 objColor;
    float shininess;
    
    //折射计算参数:材料折射率
    float n;
    
    Material(int type) : type(type) {}
    // 粗糙材质
    Material(glm::vec3 color, float shininess):type(ROUGH),
        objColor(color), shininess(shininess), R0(glm::vec3(.0f, .0f, .0f)){}
    // 反射材质
    Material(glm::vec3 R0) : type(REFLECTIVE), R0(R0) {}
    // 折射材质
    Material(float n) : type(REFRACTIVE), n(n) {float r0 = pow((n - 1) / (n + 1), 2); R0 = glm::vec3(r0, r0, r0);}
};

1.3 光照 Light

光照数据结构为场景中的光照,在本项目中为平行光,故只需定义光的方向、颜色和光照强度,在本项目中把光照颜色和强度定义在一起了,所以本项目中的光照结构体只有两个参数。光照结构体Light定义如下:

struct Light{
    glm::vec3 dir;     //光照方向
    glm::vec3 color;   //光照强度*颜色
    
    Light(glm::vec3 dir, glm::vec3 color) : dir(glm::normalize(dir)), color(color) {}
};

1.4 相机 Camera

本项目中的相机类参考了官方教程中给出的相机类,并针对本项目进行了简化。本项目中相机指向场景的坐标原点,其主要函数为构造函数和观察光线函数getRay,分别实现了通过相机位置构造相机对象、返回观察光线的功能。相机类Camera的定义如下:

class Camera{
public:
    // 相机位置、旋转等参数
    glm::vec3 position;
    glm::vec3 front;
    glm::vec3 up;
    glm::vec3 right;
    glm::vec3 worldUp;
    // 相机构造函数
    Camera(glm::vec3 position = glm::vec3(.0f, .0f, 1.0f))
    : front(-glm::normalize(position)), position(position), worldUp(glm::vec3(.0f, 1.0f, .0f)) {updateCameraVectors();}
    // 相机在xOz平面围绕坐标原点进行旋转    
    void rotate(float radian);
    // 返回观察光线
    Ray getRay(float viewx, float viewy, float fov);
    
private:
    // 调整相机状态
    void updateCameraVectors(void);
};

1.5 球体Sphere

球体为本项目中用于光线求交等计算的物体,球体包含球心、半径、材质等基本参数,拥有友元函数求交函数,用于计算光线是否与球体相交,并记录交点的坐标、法向量参数,求交函数将在算法实现部分进行详细说明。球体类Sphere的定义如下:

class Sphere{
    // 球体参数:球心、半径、材质
    glm::vec3 center;
    float radius;
    
public:
    Material *material;
    // 球体构造函数
    Sphere(glm::vec3 center, float radius, Material *material)
        : center(center), radius(radius), material(material) {}
    // 友元函数:判断光线与球体碰撞
    friend float Intersect(const Sphere &sphere, const Ray &ray, glm::vec3 *hitNorm, glm::vec3 *hitPos);
};

1.6 场景Scene

场景类存放以上定义的球体、光照、摄像机等类,也是光线追踪算法进行的地方。场景类包含球体可变数组、光照可变数组(场景中可添加多个光照)、相机等成员,以及背景颜色属性;场景类有首次相交、阴影检测、光线追踪等函数与方法,用于场景中进行光线追踪计算(以上函数与方法的具体实现将在算法实现中进行详细说明)。场景类Scene的定义如下:

class Scene{
    // 场景类基本成员:球体、光照、相机,以及背景颜色
    std::vector<Sphere*> objects;
    std::vector<Light*> light;
    Camera camera;
    glm::vec3 background_color;
    
public:
    Scene();
    // 设置场景参数
    void setLight(Light *light);
    void addSphere(Sphere *sphere);
    void setBackground(glm::vec3 bgcolor = glm::vec3(.0f, .0f, .0f));
    // 检测光线与场景中物体的碰撞
    bool SceneIntersect(const Ray &ray,
                        int *hitObject, glm::vec3 *hitNorm, glm::vec3 *hitPos);
    // 阴影检测
    bool ShadowRayIntersect(const Ray &shadow_ray);
    // 光线追踪
    glm::vec3 Trace(const Ray& ray, int depth = 0);
    // 对整个场景进行光线追踪算法    
    void renderScene(float viewWidth, float viewHeight, void(*handleFunc) (float, float, glm::vec3));
    // 旋转场景中的相机
    void rotateCamera(float radian);
};

2. 算法实现

2.1 光线追踪算法原理与步骤

进行光线追踪算法的实现之前,我们需要了解光线追踪算法的原理。以下将依据教材和相关资料对光线追踪算法的原理及其步骤进行简要的说明:

【计算机图形学】OpenGL递归实现光线追踪
图2 光线追踪原理示意图(图源维基百科同名词条)

注:上图中相关参数名与教材有一定出入,这里以教材为标准。

如上图所示,光线追踪算法总体分为以下几步:

1. 计算观察光线;

2. 计算光线与物体相交;

3. 进行阴影计算/Phong光照模型颜色计算/反射、折射光线计算;

4. 对于反射、折射光线递归调用光线追踪算法。

在本项目中,通过定义多个函数、方法,并调用上述定义的类、结构体中的属性值进行计算,以实现上述光线追踪算法的步骤,下面对这些函数与方法的原理与具体实现进行解释说明:

2.2 计算观察光线

观察光线,依据教材上的解释,即:

从眼睛到屏幕上一点的三维参数直线:【计算机图形学】OpenGL递归实现光线追踪

将屏幕上的待渲染的像素点坐标(u, v)由屏幕坐标系uvw投影到场景坐标系xyz,得到转化后的场景坐标以构造观察光线,教材上对于这一转化的描述如下(其中l, b分别为屏幕坐标原点的u, v坐标投影至场景坐标系中的值,r-l, t-b分别为屏幕坐标水平、垂直正方向投影至场景坐标系中的向量):

【计算机图形学】OpenGL递归实现光线追踪

【计算机图形学】OpenGL递归实现光线追踪

将上述转化在C++中实现,即相机类的成员函数getRay,该函数的参数分别为屏幕坐标上指定像素点的坐标,以及观察平面的边长(即屏幕长宽中较大的一项),函数将返回对应的观察光线。该函数的具体代码如下:

Ray Camera::getRay(float viewx, float viewy, float fov){
    glm::vec3 viewDir = front + right * (2 * (viewx + .5f) / fov - 1) +
        up * (2 * (viewy + .5f) / fov - 1) - position;
    Ray viewRay(position, viewDir);
    return viewRay;
}

2.3 光线与物体(球体)相交

依据教材描述,光线【计算机图形学】OpenGL递归实现光线追踪与球交点满足以下等式:

  【计算机图形学】OpenGL递归实现光线追踪

由于球心为、半径为R的球体可表示为:

将光线带入上述表达式,可得:

【计算机图形学】OpenGL递归实现光线追踪

【计算机图形学】OpenGL递归实现光线追踪

以上方程为一元二次方程,容易进行求解。若无实数解,则光线与球没有交点;若两解均小于0,则该球在相机视野的后方与光线相交,剔除该解;若两解相同且均大于0,则光线与球相切,仅有一交点;若两解不同且均大于0,则较小的一解为光线进入球的位置,较大的一解为光线离开球的位置。

在项目中通过球体Sphere类的友元函数Intersect对光线与球的相交进行判断。该函数的4个参数分别为求交的球体、光线,用于存储交点法向量、交点坐标的3维向量指针,函数返回光线与球体交点处的t值,若无交点则返回-1;并存储交点的法向量、坐标数据。该函数的代码实现如下:

float Intersect(const Sphere &sphere, const Ray &ray, glm::vec3 *hitNorm, glm::vec3 *hitPos){
    // 将光线的表达式p=e+t·d带入球的方程(p-c)^2-R^2=0中进行求解
    glm::vec3 dist = ray.ori - sphere.center;
    float a = glm::dot(ray.dir, ray.dir);
    float b = 2.0f * glm::dot(ray.dir, dist);
    float c = glm::dot(dist, dist) - sphere.radius * sphere.radius;
    float Delta = b * b - 4.0f * a * c;
    // 若判别式小于0,则光线与球未相交,返回-1
    if (Delta < 0) return -1;
    Delta = sqrtf(Delta);
    float t1 = (-b + Delta) / 2.0f / a;
    float t2 = (-b - Delta) / 2.0f / a;
    // 获得方程的2解,较小的t2为光线进入球的位置,较大的t1为光线离开球的位置
    // 若2解均小于0,即球位于光线后面,则返回-1
    if (t1 <= 0) return -1;
    // 光线与球相交于e+t·d处,记录交点坐标、交点法向量
    float t = t2 > 0 ? t2 : t1;
    if(hitPos != NULL)
        *hitPos = ray.ori + t * ray.dir;
    if(hitNorm != NULL)
        *hitNorm = (*hitPos - sphere.center) / sphere.radius;
    return t;
}

2.4 光线追踪算法的实现

本项目依据教材10.4-10.7章节的光线追踪程序部分内容实现光线追踪算法程序。光线追踪算法由多个函数实现,主要的包含递归调用的光线追踪函数为场景类Scene的成员函数Trace,该函数包含光线求交、反射、折射、Phong光照模型等计算,实现了完整的光线追踪算法。下面将分多个部分对本项目的光线追踪算法及其实现进行解释说明:

2.4.1 遮挡剔除

光线追踪程序首先需要找到场景中首个与该观察光线相交的物体,以便进行后续的颜色计算。在场景中,可能有多个物体与一条观察光线相交,我们需要找到场景中与光线交点t值最小(距离光线源点最近)的物体,在该物体进行后续的颜色、反射折射等计算。

【计算机图形学】OpenGL递归实现光线追踪
图3 遮挡剔除示意图(图源维基百科光线追踪词条)

教材中对于该算法的描述为以下伪代码:

hit = false

for each object o do

        if(object is hit at ray parameter t and ) then

                hit = true

                hitobject = o

                 = t

return hit

在本项目中,该算法由场景类Scene的成员函数SceneIntersect实现,该函数返回指定观察光线是否与场景中物体相交,若相交,则存储与光线首个相交的物体的数组编号、法向量与交点坐标。该函数的代码实现如下:

bool Scene::SceneIntersect(const Ray &ray,
                    int *hitObject, glm::vec3 *hitNorm, glm::vec3 *hitPos){
    if (objects.size() < 1) return false;
    
    bool hit = false;
    float first_hit = -1;
    int first_hit_obj = -1;
    glm::vec3 first_hit_pos;
    glm::vec3 first_hit_norm;
    
    // 对场景中的每一个物体求交
    for (int i = 0; i < objects.size(); ++i) {
        glm::vec3 _hit_pos;
        glm::vec3 _hit_norm;
        float t = Intersect(*objects[i], ray, &_hit_norm, &_hit_pos);
        // 若此时交点的t值小于先前记录的最小t值,则记录此t值为最小t值,并记录此球的序号与交点的相应信息
        if (t > 0 && t > TRACE_MIN_T && (first_hit < 0 || t < first_hit)) {
            hit = true;
            first_hit_obj = i;
            first_hit = t;
            first_hit_pos = _hit_pos;
            first_hit_norm = _hit_norm;
        }
    }
    
    if(hit){
        if(hitObject != NULL) *hitObject = first_hit_obj;
        if(hitPos != NULL) *hitPos = first_hit_pos;
        if(hitNorm != NULL) *hitNorm = first_hit_norm;
    }
    return hit;
}

2.4.2 阴影

根据生活常识,若一个物体在光线方向上被其他物体遮挡,则会落入阴影中。换一种方法说,即从物体表面的一个点向光源方向发出一条“阴影光线”【计算机图形学】OpenGL递归实现光线追踪,若该阴影光线与其他物体相交(即),则该点在阴影中;若阴影光线不与任何物体相交,则该点不在阴影中,对该点运用Phong光照模型进行颜色计算。由于数值的不确定性,阴影光线可能与其源点所在平面相交,故使t在范围内进行检测(ε为一个极小的正值)。

【计算机图形学】OpenGL递归实现光线追踪
图4 阴影示意图(图源维基百科)

教材上的阴影部分伪代码如下:

function raycolor(ray 【计算机图形学】OpenGL递归实现光线追踪, real , real )

hit-record rec, srec

if(scene->hit(【计算机图形学】OpenGL递归实现光线追踪, ,, rec)) then                                   //测试是否与场景中物体相交

        【计算机图形学】OpenGL递归实现光线追踪                                                               //构造阴影光线

        color c =                                                    //Phong模型环境泛光

        if(not scene->hit(【计算机图形学】OpenGL递归实现光线追踪, , , srec)) then                    //若不在阴影内

                vec3 = normalized(normalized() + normalized(-))

                c = c + Phong(rec, )                                              //通过Phong模型计算颜色

        return c

else

        return background-color                                                //若无相交,则返回背景色

上述伪代码中的Phong(rec, )表示:若点不在阴影内,则调用Phong光照模型计算该点的颜色。Phong模型包含环境泛光、漫反射、镜面高光,是一种较为逼真的着色方法,下面对Phong模型的原理与实现进行简要说明: 

【计算机图形学】OpenGL递归实现光线追踪
图5 Phong光照模型

环境泛光为物体处在场景中受周围反射光线影响的颜色,在Phong模型中被表示为环境光颜色与物体颜色的数值乘积,场景中所有物体均受到环境泛光的影响;

漫反射指粗糙表面物体向四处反射入射光的现象,在Phong模型中,漫反射仅与入射光的角度有关,与观察者位置等因素无关,在教材中,漫反射被表示为光照颜色乘以光照方向与交点法向量的点积,即:

;

镜面高光为在光滑物体表面的高光,镜面高光影响因素有观察者的观察光线与反射光的夹角和高光指数;观察者的观察光线与反射光线夹角越小,高光越亮,反之亦然,在Phong模型中使用光照方向与观察光线方向间的半角向量与交点法向量的点乘表示观察光线与反射光线的夹角;高光指数p为该材料上高光的衰减系数,与不同材料有关,p值越大高光面积越小。故完整的镜面高光可表示为:

教材中完整的Phong模型伪代码为:

【计算机图形学】OpenGL递归实现光线追踪

以下为本项目中阴影检测函数ShadowRayIntersect,该函数为简化的碰撞函数,仅返回阴影光线是否与物体相交:

bool Scene::ShadowRayIntersect(const Ray &shadow_ray){
    if (objects.size() < 1) return false;
    for (int i = 0; i < objects.size(); ++i) {
        float t = Intersect(*objects[i], shadow_ray, NULL, NULL);
        if (t > 0 && t > TRACE_MIN_T)
            return true;
    }
    return false;
}

本项目中对粗糙材质的球体进行阴影检测与Phong光照模型计算,在程序中的实现如下:

if(hit_material.type == ROUGH){
    // 环境光
    c = background_color * hit_material.objColor;
    // 测试是否为阴影
    for (int i = 0; i < light.size(); ++i) {
        Ray shadow_ray(hit_pos + EPSILON * light[i]->dir, light[i]->dir);
        if(!ShadowRayIntersect(shadow_ray)){
            // 漫反射
            float diff = fmax(glm::dot(light[i]->dir, hit_norm), .0f);
            glm::vec3 diffuse = diff * light[i]->color * hit_material.objColor;
            c += diffuse;
            // 镜面高光
            glm::vec3 view_dir = glm::normalize(ray.ori - hit_pos);
            glm::vec3 reflect_dir = glm::reflect(-light[i]->dir, hit_norm);
            float Delta = glm::dot(view_dir, reflect_dir);
            if(Delta>0){
                float specular = powf(Delta, hit_material.shininess);
                c += specular;
            }
        }
    }
    return c;
}

以下为实现阴影检测与Phong光照模型后的场景渲染效果:

【计算机图形学】OpenGL递归实现光线追踪
图6 阴影检测与Phong模型

2.4.3 镜面反射

【计算机图形学】OpenGL递归实现光线追踪
图7 镜面反射原理

镜面反射原理非常简单,在已知入射光线方向与法向量的情况下,反射光线可被表示为:

【计算机图形学】OpenGL递归实现光线追踪

对反射光线递归调用光线追踪函数,以检测与反射光线相交的物体,计算反射光线的颜色;由于场景中可能存在多个反射物体,导致递归调用不能及时终止、使算法效率低下甚至进入无限递归,在递归调用光线追踪算法时,设置最大递归深度,每次进行递归调用时递归深度+1,若某次递归调用时的递归深度高于最大递归深度,则递归调用终止;最后,与阴影检测类似,为了不使反射光线与碰撞点所在平面相交,将反射光线碰撞检测t值的范围设置为。

由于在现实中不同材质对光线的反射效果不同,例如水的反射效果不如钢材、金反射黄色光线的效果高于其他颜色的光,故使用球体材质的材质颜色(objColor)属性值以表示当前材质对光线的反射效果。

上述镜面反射有关算法的伪代码如下:

【计算机图形学】OpenGL递归实现光线追踪

在本项目中,镜面反射相关功能实现如下(最大递归深度取值为5):

// 计算反射光线方向(入射光线方向与法线方向点乘结果为负,故使用减法)
glm::vec3 reflectDir = ray.dir - 2 * glm::dot(ray.dir, hit_norm) * hit_norm;
Ray reflectRay = Ray(hit_pos + EPSILON * reflectDir, glm::normalize(reflectDir));
glm::vec3 cs = hit_material.objColor;
// 递归调用光线追踪函数
c = cs * Trace(reflectRay, depth + 1);

实现阴影、Phong模型与镜面反射后的渲染效果如下:

【计算机图形学】OpenGL递归实现光线追踪
图8 镜面反射效果

上图场景中将底部与后方物体材质设置为反射材质,可以在这两个物体表面看到其他物体的镜像。

2.4.4 折射

【计算机图形学】OpenGL递归实现光线追踪
图9 折射定律(图源教材)

折射光线的方向可以使用折射定律(Snell法则)求得。取空气的折射率=1,折射定律在本项目中可表示为:sinθ=sinφ,经过一系列化简可以得出折射光线的方向可表示为:

【计算机图形学】OpenGL递归实现光线追踪

根据菲涅尔公式的Shlick近似可以计算出折射材质折射光线和反射光线的颜色(这个公式的原理太复杂了我看不懂,但是在这个项目中我们只要会用就行了),该公式表示如下:

【计算机图形学】OpenGL递归实现光线追踪

其中的为法线上的反射系数,在折射材质中可表示为:

【计算机图形学】OpenGL递归实现光线追踪

根据以上公式,折射材质的颜色可表示为:

【计算机图形学】OpenGL递归实现光线追踪

在反射计算中添加折射材质的反射计算,其cs值即R0通过其n值计算得到;添加了折射材质的计算后,本项目中光线追踪算法的反射与折射部分如下:

// 计算反射光线方向(入射光线方向与法线方向点乘结果为负,故使用减法)
glm::vec3 reflectDir = ray.dir - 2 * glm::dot(ray.dir, hit_norm) * hit_norm;
Ray reflectRay = Ray(hit_pos + EPSILON * reflectDir, glm::normalize(reflectDir));
// 使用菲涅尔方程的Shlick近似方程计算反射光线颜色
glm::vec3 cs;
float cosTheta = - glm::dot(glm::normalize(ray.dir), hit_norm);
cs = hit_material.R0 + (glm::vec3(1.0f, 1.0f, 1.0f) - hit_material.R0) * pow(1.0f - cosTheta, 5.0f);
// 递归调用光线追踪函数
c = cs * Trace(reflectRay, depth + 1);

if(objects[hit_index]->material->type == REFRACTIVE){
    float cos2Phi = 1 - (1 - cosTheta * cosTheta) / (hit_material.n * hit_material.n);
    if(cos2Phi >= 0){
        glm::vec3 refractDir = (ray.dir - hit_norm * glm::dot(ray.dir, hit_norm)) / hit_material.n
            - hit_norm *sqrtf(cos2Phi);
        Ray refractRay = Ray(hit_pos - EPSILON * refractDir, glm::normalize(refractDir));
        c += (glm::vec3(1.0f, 1.0f, 1.0f) - cs) * Trace(refractRay, depth + 1);
    }
}

return c;

实现阴影、Phong模型、反射与折射后的渲染效果如下:

【计算机图形学】OpenGL递归实现光线追踪
图10 折射

 2.4.5 包含递归调用的光线追踪函数

综合上述Phong光照模型、阴影、反射、折射等部分,可以得到完整的包含递归调用的光线追踪函数。该函数在本项目中的完整代码如下:

glm::vec3 Scene::Trace(const Ray& ray, int depth){
    // 递归次数达到上限,返回环境泛光值
    if(depth > TRACE_MAX_DEPTH)
        return background_color;
    int hit_index;
    glm::vec3 hit_pos, hit_norm;
    // 光线未击中任何物体,返回背景色
    if(!SceneIntersect(ray, &hit_index, &hit_norm, &hit_pos))
        return background_color;
    
    glm::vec3 c;
    Material hit_material = *(objects[hit_index]->material);
    
    if(hit_material.type == ROUGH){
        // 环境光
        c = background_color * hit_material.objColor;
        // 测试是否为阴影
        for (int i = 0; i < light.size(); ++i) {
            Ray shadow_ray(hit_pos + EPSILON * light[i]->dir, light[i]->dir);
            if(!ShadowRayIntersect(shadow_ray)){
                // 漫反射
                float diff = fmax(glm::dot(light[i]->dir, hit_norm), .0f);
                glm::vec3 diffuse = diff * light[i]->color * hit_material.objColor;
                c += diffuse;
                // 镜面高光
                glm::vec3 view_dir = glm::normalize(ray.ori - hit_pos);
                glm::vec3 reflect_dir = glm::reflect(-light[i]->dir, hit_norm);
                float Delta = glm::dot(view_dir, reflect_dir);
                if(Delta>0){
                    float specular = powf(Delta, hit_material.shininess);
                    c += specular;
                }
            }
        }
        return c;
    }

    // 计算反射光线方向(入射光线方向与法线方向点乘结果为负,故使用减法)
    glm::vec3 reflectDir = ray.dir - 2 * glm::dot(ray.dir, hit_norm) * hit_norm;
    Ray reflectRay = Ray(hit_pos + EPSILON * reflectDir, glm::normalize(reflectDir));
    // 使用菲涅尔方程的Shlick近似方程计算反射光线颜色
    glm::vec3 cs;
    float cosTheta = - glm::dot(glm::normalize(ray.dir), hit_norm);
    cs = hit_material.R0 + (glm::vec3(1.0f, 1.0f, 1.0f) - hit_material.R0) * pow(1.0f - cosTheta, 5.0f);
    // 递归调用光线追踪函数
    c = cs * Trace(reflectRay, depth + 1);
    
    // 折射计算
    if(objects[hit_index]->material->type == REFRACTIVE){
        float cos2Phi = 1 - (1 - cosTheta * cosTheta) / (hit_material.n * hit_material.n);
        if(cos2Phi >= 0){
            glm::vec3 refractDir = (ray.dir - hit_norm * glm::dot(ray.dir, hit_norm)) / hit_material.n
                - hit_norm *sqrtf(cos2Phi);
            Ray refractRay = Ray(hit_pos - EPSILON * refractDir, glm::normalize(refractDir));
            c += (glm::vec3(1.0f, 1.0f, 1.0f) - cs) * Trace(refractRay, depth + 1);
        }
    }
    
    return c;
}

2.5 其他

定义函数renderScene对屏幕中每一个像素进行光线追踪计算,该函数具有3个参数,分别为屏幕的宽度、高度以及程序主函数中对于计算所得的像素颜色的处理函数的句柄handleFunc。该处理函数在主程序中定义并实现,用于存储经过光线追踪算法计算所得的像素元素数据,以便后续使用OpenGL进行绘制。

void Scene::renderScene(float viewWidth, float viewHeight, void(*handleFunc) (float, float, glm::vec3)) {
    float fov = fmax(viewWidth, viewHeight);
    for (int viewx = 0; viewx < fov; viewx++) {
        for (int viewy = 0; viewy < fov; viewy++) {
            Ray viewRay = camera.getRay(viewx, viewy, fov);
            glm::vec3 pixelColor = Trace(viewRay);
            glm::vec2 cameraCoord = glm::vec2(1, 0) * (2 * (viewx + .5f) / fov - 1) +
                            glm::vec2(0, 1) * (2 * (viewy + .5f) / fov - 1);
            float renderX = fov == viewWidth ? cameraCoord.x : cameraCoord.x / viewWidth * viewHeight;
            float renderY = fov == viewHeight ? cameraCoord.y : cameraCoord.y / viewHeight * viewWidth;
            (*handleFunc) (renderX, renderY, pixelColor);
        }
    }
}

我使用的绘制方法是以点绘制,由于显示器的原因,如果使用1:1的比例进行绘制的话绘制结果会出现大量空隙影响观感,所以在renderScene函数中把屏幕尺寸设置为了真实窗口尺寸的2倍,推测使用Texture绘制能够避免这一问题。

//于函数renderScene中调用,用于将计算所得的像素点颜色与坐标加入vertices向量中
void loadPixel(float fx, float fy, glm::vec3 fragColor){
    vertices.push_back(fx);
    vertices.push_back(fy);
    vertices.push_back(.0f);
    vertices.push_back(fragColor.x);
    vertices.push_back(fragColor.y);
    vertices.push_back(fragColor.z);
}

在main函数中进行场景初始化并添加球体,成功运行程序后能够得到文章一开始时的画面:

Scene mainScene;
mainScene.setBackground(glm::vec3(.461f, .141f, .062f));
mainScene.setLight(new Light(glm::vec3(.5f, 1.0f, .5f), glm::vec3(1.5f, 1.5f, 1.5f)));
mainScene.addSphere(new Sphere(glm::vec3(.2f, .0f, .0f), .1f, roughMaterial(blue)));
mainScene.addSphere(new Sphere(glm::vec3(-.2f, .0f, .0f), .1f, roughMaterial(red)));
mainScene.addSphere(new Sphere(glm::vec3(.0f, .0f, -.2f), .1f, roughMaterial(brown)));
mainScene.addSphere(new Sphere(glm::vec3(.0f, .0f, .2f), .1f, defaultRefractive));
mainScene.addSphere(new Sphere(glm::vec3(.0f, .2f, .0f), .1f, metalReflective));
mainScene.addSphere(new Sphere(glm::vec3(.0f, -200.15f, .0f), 200.0f, defaultReflective));
mainScene.rotateCamera(.125 * PI);
vertices.clear();
mainScene.renderScene(2 * WINDOW_WIDTH, 2 * WINDOW_HEIGHT, loadPixel);

三、总结

总体实现了课程设计中的要求,由于没有做反走样,存在一定量的噪点,也是本项目中的不足。

文章中的参考资料均在参考时写出。

第一次在这个平台写博客,文章存在一定的纰漏与错误,请见谅。

项目地址:项目地址文章来源地址https://www.toymoban.com/news/detail-478855.html

到了这里,关于【计算机图形学】OpenGL递归实现光线追踪的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 计算机图形学与opengl C++版 学习笔记 第12章 曲面细分

    术语Tessellation(镶嵌)是指一大类设计活动,通常是指在平坦的表面上,用各种几何形状的瓷砖相邻排列以形成图案。它的目的可以是艺术性的或实用性的,很多例子可以追溯到几千年前[TS16]。 在3D图形学中,Tessellation指的是有点不同的东西(曲面细分),但显然是由它的经

    2024年02月08日
    浏览(29)
  • 计算机图形学与opengl C++版 学习笔记 第11章 参数曲面

    在20世纪50年代和60年代在雷诺公司工作期间,皮埃尔·贝塞尔(Pierre Bézier)开发了用于设计汽车车身的软件系统。他的程序利用了Paul de Casteljau之前开发的数学方程组,后者曾为竞争对手雪铁龙汽车制造商[BE72,DC63]工作。de Casteljau方程仅使用几个标量参数描述曲线,同时使用

    2024年02月08日
    浏览(37)
  • 计算机图形学与opengl C++版 学习笔记 第9章 天空和背景

    对于室外3D场景,通常可以通过在地平线上创造一些逼真的效果,来增强其真实感。当我们极目远眺,目光越过附近的建筑和森林,我们习惯于看到远处的大型物体,例如:云、群山或太阳(或夜 空中的星星和月亮)。但是,将这些对象作为单个模型添加到场景中可能会产生

    2024年02月09日
    浏览(31)
  • 计算机图形学与opengl C++版 学习笔记 第10章 增强表面细节

    假设我们想要对不规则表面的物体进行建模,例如橘子凹凸的表皮、葡萄干褶皱的表面或月球的陨石坑表面。我们该怎么做?到目前为止,我们已经学会了两种可能的方法: (a)我们可以对整个不规则表面进行建模,但这么做通常不切实际(一个有许多坑的表面需要大量的

    2024年02月09日
    浏览(34)
  • 【计算机视觉】---OpenCV实现物体追踪

    OpenCV中的物体追踪算法基于视觉目标跟踪的原理。物体追踪的目标是在连续的图像序列中定位和跟踪特定物体的位置。 在物体追踪中,我们需要对目标对象进行表示。通常使用边界框(bounding box)来表示目标的位置和大小。边界框是一个矩形区域,由左上角的坐标(x,y)和

    2024年02月08日
    浏览(40)
  • 计算机图形学:二维图形的几何变换(算法原理及代码实现)

    对于一个二维图形作平移、旋转、放缩变换,可以转换为在二维坐标系中图形的所有点分别可以对应到在x,y轴方向分别平移tx,ty(平移)、绕一点旋转固定的角(旋转)、在x,y轴方向分别放缩sx,sy倍。 对于变换的原理,只需要将原图形的点通过极坐标或者相加、相乘,再

    2024年02月11日
    浏览(29)
  • 计算机图形学:三次Bezier曲线的绘制(算法原理及代码实现)

    一、实现方案        贝塞尔曲线原理:贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。它通过控制曲线上的四个点(起始点、终止点以及两个相互分离的中间点)来创造、编辑图形。其中起重要作用的是位于曲线中央的控制线。这条

    2024年02月11日
    浏览(36)
  • 计算机视觉项目-实时目标追踪

    😊😊😊 欢迎来到本博客 😊😊😊 本次博客内容将继续讲解关于OpenCV的相关知识 🎉 作者简介 : ⭐️⭐️⭐️ 目前计算机研究生在读。主要研究方向是人工智能和群智能算法方向。目前熟悉深度学习(keras、pytorch、yolo系列),python网页爬虫、机器学习、计算机视觉(Op

    2024年02月03日
    浏览(27)
  • 【opencv】计算机视觉:实时目标追踪

    目录 前言 解析 深入探究 前言 目标追踪技术对于民生、社会的发展以及国家军事能力的壮大都具有重要的意义。它不仅仅可以应用到体育赛事当中目标的捕捉,还可以应用到交通上,比如实时监测车辆是否超速等!对于国家的军事也具有一定的意义,比如说导弹识别目标等

    2024年02月05日
    浏览(34)
  • 计算机图形学实验——利用MFC对话框实现多边形绘制与填充(扫描线填充算法)附源码

    内容概括: 利用基于对话框的MFC项目 实现鼠标点击绘制多边形 实现扫描线算法填充多边形 源码见Yushan-Ji/ComputerGraphics: ECNU2023秋 计算机图形学课程实验代码 (github.com) 通过鼠标交互输入多边形 对各种多边形进行填充,包括边界自交的情况 利用 OnLButtonDown 和 OnRButtonDown 函数,

    2024年02月04日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包