zoukankan      html  css  js  c++  java
  • 简单的图形学(二)——材质与反射

    在上一篇【游戏框架系列】简单的图形学(一)文章中,我们讲述了光线追踪的一个最简单的操作——依每个像素延伸出一条追踪光线,光线打到球上(产生交点),就算出这条线的长度,作为最终的灰度,打不到球上,就显示为黑色

    仓库:bajdcc/GameFramework

    本节代码:https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/RenderMaterial.cpp

    几何图形算法接口:https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Geometries.h

    无论是材质,还是反射,还是折射等,无非就是在这个交点处依不同算法计算出颜色而已,这个颜色最终会显示到屏幕上对应的像素上。下面介绍的就是计算交点处颜色的方法。

    参考自miloyip的文章用JavaScript玩转计算机图形学(一)光线追踪入门 - Milo Yip - 博客园

    实现材质

    v2-406c25c9dbed9bf46ff6583f90e79a2a_rPhong材质效果

    这里就实现两种材质:棋盘和Phong,老实说,我都没听说过Phong这个东西,应该又是纯数学推导出来的一种结论吧。

    先写好接口:

    // 材质接口
    class Material
    {
    public:
        Material(float reflectiveness);
        virtual ~Material();
        virtual color Sample(Ray ray, vector3 position, vector3 normal) = 0;
    
        float reflectiveness;
    };
    
    // 棋盘材质
    class CheckerMaterial : public Material
    {
    public:
        CheckerMaterial(float scale, float reflectiveness);
    
        color Sample(Ray ray, vector3 position, vector3 normal) override;
    
        float scale;
    };
    
    // Phong材质
    class PhongMaterial : public Material
    {
    public:
        PhongMaterial(color diffuse, color specular, float shininess, float reflectiveness);
    
        color Sample(Ray ray, vector3 position, vector3 normal) override;
    
        color diffuse;
        color specular;
        float shininess;
    };
    

    棋盘是在平面上的,所以我们事先要实现一个光线与平面的相交算法。

    判断直线与平面相交

    IntersectResult Plane::Intersect(Ray ray)
    {
        const auto a = DotProduct(ray.direction, normal);
    
        if (a >= 0)
            // 反方向看不到平面,负数代表角度为钝角
            // 举例,平面法向量n=(0,1,0),距离d=0,
            // 我从上面往下看,光线方向为y轴负向,而平面法向为y轴正向
            // 所以两者夹角为钝角,上面的a为cos(夹角)=负数,不满足条件
            // 当a为0,即视线与平面平行时,自然看不到平面
            // a为正时,视线从平面下方向上看,看到平面的反面,因此也看不到平面
            return IntersectResult();
    
        // 参考 http://blog.sina.com.cn/s/blog_8f050d6b0101crwb.html
        /* 将直线方程写成参数方程形式,即有:
           L(x,y,z) = ray.origin + ray.direction * t(t 就是距离 dist)
           将平面方程写成点法式方程形式,即有:
           plane.normal . (P(x,y,z) - plane.position) = 0
           解得 t = {(plane.position - ray.origin) . normal} / (ray.direction . plane.normal )
        */
        const auto b = DotProduct(normal, ray.origin - position);
    
        const auto dist = -b / a;
        return IntersectResult(this, dist, ray.Eval(dist), normal);
    }
    

    纯数学推导较多,这里有个优化:先看光线方向和平面法向量方向是否同向(夹角为锐角),是则直接判定不相交。

    确定好算法后,看看棋盘材质的实现:

    color CheckerMaterial::Sample(Ray ray, vector3 position, vector3 normal)
    {
        static color black(Gdiplus::Color::Black);
        static color white(Gdiplus::Color::White);
        return fabs(int(floorf(position.x * 0.1f) + floorf(position.z * scale)) % 2) < 1 ? black : white;
    }
    

    简单来说,就是“x坐标+z坐标”取整是否是2的倍数。

    我们主要分析下材质接口需要哪些成分:

    1. 光线Ray,即入射光线的起点位置和方向、交点position、交点法向normal,光线方向和交点法向量是为了计算反射、折射。
    2. 交点位置 position
    3. 交点法向 normal

    接下来看看Phong材质,参考里面的网址说明,完全的数学公式。

    color PhongMaterial::Sample(Ray ray, vector3 position, vector3 normal)
    {
        /*
          参考 https://www.cnblogs.com/bluebean/p/5299358.html Blinn-Phong模型
            Ks:物体对于反射光线的衰减系数
            N:表面法向量
            H:光入射方向L和视点方向V的中间向量
            Shininess:高光系数
    
            Specular = Ks * lightColor * pow(dot(N, H), shininess)
    
            当视点方向和反射光线方向一致时,计算得到的H与N平行,dot(N,H)取得最大;当视点方向V偏离反射方向时,H也偏离N。
            简单来说,入射光与视线的差越接近法向量,镜面反射越明显
         */
    
        const auto NdotL = DotProduct(normal, lightDir);
        const auto H = Normalize(lightDir - ray.direction);
        const auto NdotH = DotProduct(normal, H);
        const auto diffuseTerm = diffuse * fmax(NdotL, 0.0f); // N * L 入射光在镜面法向上的投影 = 漫反射
        const auto specularTerm = specular * powf(fmax(NdotH, 0.0f), shininess);
        return lightColor * (diffuseTerm + specularTerm);
    }
    

    计算时需要四个参数:

    1. Ks:物体对于反射光线的衰减系数
    2. N:表面法向量
    3. H:光入射方向L和视点方向V的中间向量
    4. Shininess:高光系数

    我们看到的Phong材质颜色其实是它表面反射出的光,如何计算反射的光什么颜色?

    Phong材质有三个参数:diffuse漫反射颜色、specular镜面反射颜色、shininess高光系数(其实就是调节前两者的混合比例)。如题图中所示,diffuse就是球本身材质的颜色,而specular就是外界光打上去的颜色。因此,这里会假设有一个外界光源lightColor/lightDir,故而在上述计算过程中会用到它。

    实现反射

    v2-7cae800a62bc80f6fa158b3281c645fc_r反射效果

    看到反射,其实就是用递归实现的,同时要限制递归的深度。

    color PhysicsEngine::RenderReflectRecursive(World& world, const Ray& ray, int maxReflect)
    {
        static color black(Gdiplus::Color::Black);
    
        auto result = world.Intersect(ray);
    
        if (result.body) {
            // 参见 https://www.cnblogs.com/bluebean/p/5299358.html
    
            // 取得反射系数
            const auto reflectiveness = result.body->material->reflectiveness;
    
            // 先采样(取物体自身的颜色)
            auto color = result.body->material->Sample(ray, result.position, result.normal);
    
            // 加上物体自身的颜色成份(与反射的颜色相区分)
            color = color * (1.0f - reflectiveness);
    
            if (reflectiveness > 0 && maxReflect > 0) {
    
                // 公式 R = I - 2 * N * (N . I) ,求出反射光线
                const auto r = result.normal * (-2.0f * DotProduct(result.normal, ray.direction)) + ray.direction;
    
                // 以反射光线作为新的光线追踪射线
                const auto reflectedColor = RenderReflectRecursive(world, Ray(result.position, r), maxReflect - 1);
    
                // 加上反射光的成份
                color = color + (reflectedColor * reflectiveness);
            }
            return color;
        }
        return black;
    }
    

    如代码中所示,基本思路是:

    1. 光线与几何物体有交点吗?
    2. 没有交点:返回黑色
    3. 有交点:对自身材质采样,占比为(1-反射系数),计算出反射光线,继续追踪,并将追踪到的采样占比为(反射系数)

    即:

    color reflect_sample(world, ray, 深度)
    {
    __ 如果world和ray不相交, 返回 黑色
    __ 如果world和ray相交,假设这个相交物体为body
    ____ 1. 加上 body的材质颜色 * 比例(1.0f - reflectiveness)
    ____ 2. 计算出反射光线ray_f
    ____ 3. 加上反射颜色reflect_sample(world, ray_f, 深度-1) * 比例(reflectiveness)
    }

    这里重点是根据入射光线和法向量求反射光线,这是道数学題,参考过程在代码中的网址中。

    到这里,miloyip的光线追踪入门就尝试完成了,接下来是讲述基本光源。

    https://zhuanlan.zhihu.com/p/31012319备份。

  • 相关阅读:
    POJ 3616 Milking Time(简单区间DP)
    AizuOJ ALDS1_7_A Rooted Trees(有根树的表达)
    jQuery中 attr() 和 prop() 的区别
    前后端交互模式
    快速排序
    冒泡排序实现
    Vue 组件间进行通信
    JavaScript 数组常用方法
    如何将内网映射到公网?
    javax.mail.AuthenticationFailedException: 535 Login Fail. Please enter your authorization code to login. More information in
  • 原文地址:https://www.cnblogs.com/bajdcc/p/8973003.html
Copyright © 2011-2022 走看看