• 软件光栅器实现(二、VS和PS的运作,法线贴图,切空间的计算)


    二、软件光栅器的VS和PS的输入、输出和运作,实现法线贴图效果的版本。转载请注明出处

      这里介绍的VS和PS是实现法线映射的版本,本文仅介绍实现思路,并给出代码供参考。切空间计算、光照模型等相关公式不是本文重点,本文暂不给出,读者可以查阅其他博文或文献。

      软光栅的顶点部分处理放在VS也就是顶点着色器中进行,输入顶点的数据结构:

    //顶点信息 包括坐标,颜色,纹理坐标,法线等等
    class VertexIn
    {
    public:
        //顶点位置
        ZCVector pos;
        //顶点颜色
        ZCVector color;
        //纹理坐标
        ZCFLOAT2 tex;
        //法线
        ZCVector normal;
    
        ZCVector tangent;
        //ZCVector bitangent;
    
        VertexIn() = default;
        VertexIn(ZCVector pos, ZCVector color, ZCFLOAT2 tex, ZCVector normal)
            :pos(pos), color(color), tex(tex), normal(normal) {}
    
        VertexIn(const VertexIn& rhs):pos(rhs.pos),color(rhs.color),tex(rhs.tex),normal(rhs.normal){}
    };

      输入数据结构带有normal和tangent成员,分别表示三角形各个顶点的法线和切向量坐标,切向量是基于各点的纹理坐标由切空间公式算出来的,关于算切空间公式网上已有许多,还可以参考《3D游戏中的数学方法》一书,这里贴出实现代码:

    for (int i = indexStart; i < indexCount / 3; ++i)//计算顶点的tb    
    { VertexIn p1
    = m_vertices[vertexStart + m_indices[3 * i]]; VertexIn p2 = m_vertices[vertexStart + m_indices[3 * i + 1]]; VertexIn p3 = m_vertices[vertexStart + m_indices[3 * i + 2]]; //通过纹理和顶点坐标计算出tangent和bitangent,继而得到tbn矩阵 ZCVector Q1 = p2.pos - p1.pos;//顶点相减w仍为1 ZCVector Q2 = p3.pos - p1.pos; float s1 = p2.tex.u - p1.tex.u; float t1 = p2.tex.v - p1.tex.v; float s2 = p3.tex.u - p1.tex.u; float t2 = p3.tex.v - p1.tex.v; float ratio = 1 / (s1*t2 - s2*t1); ZCVector T;//sdir T.x = (t2 * Q1.x - t1 * Q2.x) * ratio;//t2*Q1x-t1*Q2x T.y = (t2 * Q1.y - t1 * Q2.y) * ratio; T.z = (t2 * Q1.z - t1 * Q2.z) * ratio; ZCVector B;//tdir B.x = (s1 * Q2.x - s2 * Q1.x) * ratio; B.y = (s1 * Q2.y - s2 * Q1.y) * ratio; B.z = (s1 * Q2.z - s2 * Q1.z) * ratio; Tv[indexStart + m_indices[3 * i]] = Tv[indexStart + m_indices[3 * i]] + T;//计算每个顶点的tangent向量.加上uv镜像代码时这一段注释掉 Bv[indexStart + m_indices[3 * i]] = Bv[indexStart + m_indices[3 * i]] + B; Tv[indexStart + m_indices[3 * i + 1]] = Tv[indexStart + m_indices[3 * i + 1]] + T; Bv[indexStart + m_indices[3 * i + 1]] = Bv[indexStart + m_indices[3 * i + 1]] + B; Tv[indexStart + m_indices[3 * i + 2]] = Tv[indexStart + m_indices[3 * i + 2]] + T; Bv[indexStart + m_indices[3 * i + 2]] = Bv[indexStart + m_indices[3 * i + 2]] + B;

          Tv[vertexStart + m_indices[3 * i]].Normalize();//对每个点的Tv和Bv归一化
          Bv[vertexStart + m_indices[3 * i]].Normalize();
          Tv[vertexStart + m_indices[3 * i + 1]].Normalize();
          Bv[vertexStart + m_indices[3 * i + 1]].Normalize();
          Tv[vertexStart + m_indices[3 * i + 2]].Normalize();
          Bv[vertexStart + m_indices[3 * i + 2]].Normalize();

    
    

          p1.tangent = Tv[vertexStart + m_indices[3 * i]];
          p2.tangent = Tv[vertexStart + m_indices[3 * i + 1]];
          p3.tangent = Tv[vertexStart + m_indices[3 * i + 2]];

    
    

          p1.normal.Normalize();
          p2.normal.Normalize();
          p3.normal.Normalize();

    
    

          //算三角形累加后的偏手性,存储在w分量中
          ZCVector crossNT = p1.normal.Cross(p1.tangent);
          p1.tangent.w = (crossNT.Dot(Bv[vertexStart + m_indices[3 * i]]) < 0.0f) ? -1.0f : 1.0f;
          crossNT = p2.normal.Cross(p2.tangent);
          p2.tangent.w = (crossNT.Dot(Bv[vertexStart + m_indices[3 * i + 1]]) < 0.0f) ? -1.0f : 1.0f;
          crossNT = p3.normal.Cross(p3.tangent);
          p3.tangent.w = (crossNT.Dot(Bv[vertexStart + m_indices[3 * i + 2]]) < 0.0f) ? -1.0f : 1.0f;

        }

      这段代码(进入VS处理之前)还包含有副切向量的计算,这里有两种处理的方法。①副切向量Bitangent(向量B)可以在此时,也就是输入VS之前算出来,存储到输入顶点的数据结构中,参与之后的运算;②也可以只存储法线和切向量,在之后要用到副切向量时,通过施密特正交化的方法,通过法线和切线叉乘求出来。一般来说,我们用第二种方法,因为一般一个视口内的点有许多许多,能在让一个点存储的数据量越少越好,所以可以之存储法线和切向量。此处代码把副切向量B求出是为了让读者理解向量B的意义。

      VS输出顶点的数据结构:

    //经过顶点着色器输出的结构
    class VertexOut
    {
    public:
        //世界变换后的坐标
        ZCVector posTrans;
        //投影变换后的坐标
        ZCVector posH;
        //纹理坐标
        ZCFLOAT2 tex;
        //法线
        ZCVector normal;
        //颜色
        ZCVector color;
        //1/z用于深度测试
        float oneDivZ;
    
        ZCVector viewInTangent;
        ZCVector lightInTangent;//新定义切线和副切线成员
    }

      输出顶点维护有视线向量的切向量和光线向量的切向量,它们在VS里进行计算:

    VertexOut BoxShader::VS(const VertexIn& vin)//参考https://github.com/zhangbaochong/Tiny3D
    {
        VertexOut out;
        //out.normal = vin.normal;
    
        //顶点到观察点向量
        ZCVector ViewDir = (m_eyePos - vin.pos).Normalize();
        m_dirLight.direction = ZCVector(-0.57735f, -0.57735f, 0.57735f, 0.f);
        //m_dirLight.direction = ZCVector(-0.57735f, -0.57735f, 0.57735f, 0.f);
        ZCVector LightDir = (-m_dirLight.direction).Normalize();
    
        ZCVector ViewDirInModel = ViewDir*m_worldInvTranspose;//世界到模型空间
        ZCVector LightDirInModel = LightDir*m_worldInvTranspose;//世界到模型空间
    
        ZCVector vinTangent = vin.tangent - vin.normal*vin.tangent.Dot(vin.normal);//t和n正交化一下
        ModifyZero(vinTangent);
    
        ZCVector vinBitangent = vin.normal.Cross(vin.tangent)*vin.tangent.w;
        ModifyZero(vinBitangent);
    
        ZCMatrix TBN = {//不需要正交化
            vinTangent.x, vinBitangent.x, vin.normal.x, 0,
            vinTangent.y, vinBitangent.y, vin.normal.y, 0,
            vinTangent.z, vinBitangent.z, vin.normal.z, 0,
            0, 0, 0, 1
        };
    
        ZCMatrix TBNINInvTranspose = MathUtil::ZCMatrixTranspose(MathUtil::ZCMatrixInverse(TBN));
        ZCVector ViewDirInTan = ViewDirInModel*TBNINInvTranspose;//模型空间到切空间
        ZCVector LightDirInTan = LightDirInModel*TBNINInvTranspose;//模型空间到切空间
    
        out.viewInTangent = ViewDirInTan;//存储切空间下的视线向量值
        out.lightInTangent = LightDirInTan;//存储切空间下的光线向量值
    
        out.posH = vin.pos * m_worldViewProj;
        
        out.posTrans = vin.pos * m_world;
        out.normal = out.normal * m_worldInvTranspose;
    
         out.color = vin.color;
         out.tex = vin.tex;
    
        //if (out.lightInTangent.y < 0)
        //    out.lightInTangent.y *= -1;//不知道什么原因有时候会变成负数
    
        return out;
    }

      在输入数据的阶段,定义了光线和视线等向量在世界空间下的坐标。对于视线和光线的向量,在VS阶段就把它们由世界坐标转换到了切空间中,原因在于:法线贴图上的数据,是需要逐像素处理的,所以按理来说,应该在Ps阶段中求出对应的光线和视线的切、法、副切三个分量值,得出切空间坐标,然后进行法线映射的计算。但是!PS是逐像素处理数据的,而VS是逐顶点处理数据的!一个模型渲染到屏幕上像素数肯定会远远大于顶点数,所以提前在VS阶段,也就是逐顶点处理过程中,就把模型中投在每一个点上的光线和视线的切空间分量算出来,这样就会大大节省运算成本,提高速度!到了PS中,顶点的光线和视线向量就已经在切空间中了,这样就免去了大量的计算开销。

      在VS输出之后,还要进行透视矫正(各属性要乘以z的倒数)、裁剪、确定三角形的索引,然后开始扫面三角形。在扫描三角形每条线时,就是话一个个水平的连续像素点的过程,而PS阶段就是在画点时展开的。

      PS输入顶点的数据结构就是VS的输出顶点结构。PS代码如下:

    ZCVector BoxShader::PS(VertexOut& pin)
    {
        //纹理采样
        ZCVector texColor = m_tex.Sample(pin.tex);
    
        //用采样法相贴图来代替pin.normal
        ZCVector normalColor = m_normalmap.Sample(pin.tex);
        ZCVector normalFrommap;// = { 0.f, 0.f, 0.f, 0.f };
        normalFrommap.x = normalColor.x * 2 - 1;
        normalFrommap.y = normalColor.y * 2 - 1;
        normalFrommap.z = normalColor.z * 2 - 1;
    
        m_dirLight.direction = pin.lightInTangent;//光线向量已经转成切空间,仅把光线方向赋给灯光对象
        ZCVector toEye = pin.viewInTangent;//观察向量,已经转成切空间和归一化
    
        //采样高光贴图
        ZCVector specColor = m_specmap.Sample(pin.tex);
        //衰减系数
        float atte = 0.25;
        specColor = specColor*atte;
    
        //初始化各颜色
        ZCVector ambient(0.0f, 0.0f, 0.0f, 0.0f);
        ZCVector diffuse(0.0f, 0.0f, 0.0f, 0.0f);
        //ZCVector specular(0.0f, 0.0f, 0.0f, 0.0f);//仅法线贴图,不用高光
        ZCVector specular(specColor.x, specColor.y, specColor.z, specColor.w);
    
        //光源计算后得到的环境光、漫反射 、高光
        ZCVector A, D, S;
        Lights::ComputeDirectionalLight(m_material, m_dirLight, normalFrommap, toEye, A, D, S);//法线贴图用normalFrommap
    
        ambient = ambient + A;
        diffuse = diffuse + D;
        specular = specular + S;
    
        //纹理+光照计算公式: 纹理*(环境光+漫反射光)+高光
        ZCVector litColor = texColor * (ambient + diffuse) + specular + pin.color;
    
        litColor.x = (litColor.x > 1.0f) ? 1.0f : litColor.x;
        litColor.y = (litColor.y > 1.0f) ? 1.0f : litColor.y;
        litColor.z = (litColor.z > 1.0f) ? 1.0f : litColor.z;
        litColor.w = (litColor.w > 1.0f) ? 1.0f : litColor.w;
    
        return litColor;
    }

      各像素的光照计算在Lights::ComputeDirectionalLight()中进行:

        //计算平行光
        inline void ComputeDirectionalLight(
            const Material& mat,                //材质
            const DirectionalLight& L,        //平行光,方向向量值以变换为切空间
            ZCVector normal,                    //顶点法线
            ZCVector toEye,                    //顶点到眼睛的向量
            ZCVector& ambient,                //计算结果:环境光
            ZCVector& diffuse,                //计算结果:漫反射光
            ZCVector& spec)                    //计算结果:高光
        {
            // 结果初始化为0
            ambient = ZCVector( 0.0f, 0.0f, 0.0f, 0.0f );
            diffuse = ZCVector(0.0f, 0.0f, 0.0f, 0.0f);
            spec = ZCVector(0.0f, 0.0f, 0.0f, 0.0f);
    
            // 环境光直接计算
            ambient = mat.ambient * L.ambient;
    
            // 计算漫反射系数
            float diffuseFactor = L.direction.Dot(normal);
            // 顶点背向光源不再计算
    
            if (diffuseFactor > 0.0f)
            {
                //入射光线关于法线的反射向量
                ZCVector R = MathUtil::Reflect(-L.direction, normal);
    
                float specFactor = pow(max(R.Dot(toEye), 0.0f), mat.specular.w);
    
                //计算漫反射光
                diffuse = mat.diffuse * L.diffuse * diffuseFactor;
                //计算高光
                spec = mat.specular * L.specular * specFactor;
            }
        }

      其中normal是从法线贴图中读取得到的,整个计算都是在切空间,也就是各个顶点的顶点空间中开展的。

      PS最终会返回一个颜色值,这个颜色值,通过Windows系统维护的一个图形缓存数组进行记录,从而完成最终渲染。

    m_pDevice->DrawPixel(xIndex, yIndex, m_pShader->PS(out));
    //画像素
    void Tiny3DDevice::DrawPixel(int x, int y, ZCVector color)
    {
        m_pFramebuffer[m_width*y + x] = MathUtil::ColorToUINT(color);
    }

       通过在PS中对法线贴图的读取,运用到光照模型中,最终可以得到凹凸不平的渲染效果:

    下一节(三、裁剪):https://www.cnblogs.com/zeppelin5/p/10042863.html

  • 相关阅读:
    php mysqli 查询个门店数据(面向对象)
    php 上锁 概述
    php 小程序渠道推广,转发
    php 微信公众号获取用户信息
    php 小程序获取用户信息
    php 生成小程序二维码
    1、收集日志至指定主题
    hihoCoder 网络流四·最小路径覆盖
    hihocoder1393 网络流三·二分图多重匹配
    hihocoder1398 网络流五之最大权闭合子图
  • 原文地址:https://www.cnblogs.com/zeppelin5/p/10041403.html
Copyright © 2020-2023  润新知