每羊杨

https://github.com/Zeppelin5 (Kinfu讨论群:563741937)

导航

软件光栅器实现(二、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

 

posted on 2018-11-29 22:44  每羊杨  阅读(893)  评论(0编辑  收藏  举报