二、软件光栅器的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[ * i]];
VertexIn p2 = m_vertices[vertexStart + m_indices[ * i + ]];
VertexIn p3 = m_vertices[vertexStart + m_indices[ * i + ]]; //通过纹理和顶点坐标计算出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 = / (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[ * i]] = Tv[indexStart + m_indices[ * i]] + T;//计算每个顶点的tangent向量.加上uv镜像代码时这一段注释掉
Bv[indexStart + m_indices[ * i]] = Bv[indexStart + m_indices[ * i]] + B;
Tv[indexStart + m_indices[ * i + ]] = Tv[indexStart + m_indices[ * i + ]] + T;
Bv[indexStart + m_indices[ * i + ]] = Bv[indexStart + m_indices[ * i + ]] + B;
Tv[indexStart + m_indices[ * i + ]] = Tv[indexStart + m_indices[ * i + ]] + T;
Bv[indexStart + m_indices[ * i + ]] = Bv[indexStart + m_indices[ * i + ]] + 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, .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, ,
vinTangent.y, vinBitangent.y, vin.normal.y, ,
vinTangent.z, vinBitangent.z, vin.normal.z, ,
, , ,
}; 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 * - ;
normalFrommap.y = normalColor.y * - ;
normalFrommap.z = normalColor.z * - ; 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

软件光栅器实现(二、VS和PS的运作,法线贴图,切空间的计算)的更多相关文章

  1. 软件光栅器实现(四、OBJ文件加载)

    本节介绍软件光栅器的OBJ和MTL文件加载,转载请注明出处. 在管线的应用程序阶段,我们需要设置光栅器所渲染的模型数据.这些模型数据包括模型顶点的坐标.纹理.法线和材质等等,可以由我们手动编写,也可以 ...

  2. Goland软件使用教程(二)

    Goland软件使用教程(二)一.编码辅助功能 1.      智能补全 IDE通过自动补全语句来帮助您来编写代码.快捷键“Ctrl+shift+空格”将会给你一个在当前上下文中最相关符号的列表,当您 ...

  3. yum 软件管理器

    yum软件管理器 yum是一个强大的软件包管理器,能够自动解决安装时rpm包之间的依赖关系. 一.使用yum管理软件包 1.使用命令 yum help 查看使用方法 [root@majinhai ~] ...

  4. JMeter学习-009-JMeter 后置处理器实例之 - 正则表达式提取器(二)多参数获取

    前文简述了通过后置处理器 - 正则表达式提取器 获取 HTTP请求 响应结果中的特定数据,未看过的亲,敬请参阅 JMeter学习-008-JMeter 后置处理器实例之 - 正则表达式提取器(一). ...

  5. XC软件管理器应用

    这是一个基于android 4.4开发的android应用-XC软件管理器.包含应用的信息查看,打开应用以及应用的卸载等功能.非常实用的一个应用,欢迎大家下载使用. 下载地址:http://downl ...

  6. 基于FFMPEG的跨平台播放器实现(二)

    基于FFMPEG的跨平台播放器实现(二) 上一节讲到了在Android平台下采用FFmpeg+surface组合打造播放器的方法,这一节讲一下Windows平台FFmpeg + D3D.Linux平台 ...

  7. Linux软件管理器(如何使用软件管理器来管理软件)

    我们的Linux系统大部分都是某个Linux厂商的系统,所以这些厂商可以编译好一些软件来提供用户下载,用户下载完了之后就可以直接安装,从而省去了编译源码及其过程中的各种问题.这时我们就可以使用相应的软 ...

  8. GIS基础软件及操作(十二)

    原文 GIS基础软件及操作(十二) 练习十二. ArcMap制图-地图版面设计 设置地图符号-各种渲染方式的使用 使用ArcMap Layout(布局)界面制作专题地图 将各种地图元素添加到地图版面中 ...

  9. 我终于弄懂了Python的装饰器(二)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 二 ...

随机推荐

  1. WCF输出JSON

    public class MyService : IService { public Message GetXml(string format) { WebOperationContext conte ...

  2. 20175213 2018-2019-2 《Java程序设计》第8周学习总结

    教材学习内容总结 1:泛型主要目的是建立具有类型安全的集合框架,如链表,散列映射等数据结构. 泛型类的声明: class People<E> People是泛型类的名称,E是其中泛型,E可 ...

  3. letecode242有效字母的异位词

    bool isAnagram(char* s, char* t) { ] = {}; ] = {}; int lenS = strlen(s); int lenT = strlen(t); ;i< ...

  4. 基于bootstrap的datepicker

    <script src="<%=path %>/js/bootstrap-datepicker.min.js"/> <script src=" ...

  5. Linux nfs使用krb5的方式安全挂载

    配置安全的网络nfs文件共享服务 由于本人是使用的rhce模拟考试环境来做的本题目,所以文中说到的实验脚本和评分脚本,以及krb5.keytab文件只有我本套环境独有,如果自己做练习可以不去使用实验脚 ...

  6. 通信导论-IP数据网络基础(4)

    IP地址的编址方法--IP地址+掩码地址=网络地址 分类的IP地址 每一类地址都由两个固定长度的字段组成,其中一个字段是网络号 net-id,标志主机或路由器所连接到的网络,另一个字段则是主机号 ho ...

  7. 关于微信小程序appsecret保护的问题

    本地后端代码中通常会配置 appid 和 appsecret,直接 push 到 公有 git 库会导致所有人可见.但其他人由于不是开发者有了别的项目的 secret 用处不大.但仍建议采用某种方法加 ...

  8. CentOS7 常用设置

    安装配置 0.Centos7 优盘U盘安装以及解决安装时引导错误 1.CentOS7开启网卡,设置开机启用网卡 2.CentOS7 修改静态IP地址 3.CentOS7 下使用root免密码输入自动登 ...

  9. JS判断图片是否加载完成 背景图404 快到碗里来

    面对这个问题 我最多做到表面笑嘻嘻 …… 真不知道测试怎么那么…… 啥都能给你测出来 有的没的都能给你测出来 算了算了  谁让本仙女本精灵本可爱温柔大方善解人意呢 …呵呵呵 ————————————正 ...

  10. ORACLE设置用户密码不过期

    1.查看用户的 profile 是哪个,一般是 default SELECT USERNAME, PROFILE FROM dba_users; 2.查看指定概要文件(这里是1中对应的profile) ...