软件光栅器实现(二、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[ * 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的运作,法线贴图,切空间的计算)的更多相关文章
- 软件光栅器实现(四、OBJ文件加载)
本节介绍软件光栅器的OBJ和MTL文件加载,转载请注明出处. 在管线的应用程序阶段,我们需要设置光栅器所渲染的模型数据.这些模型数据包括模型顶点的坐标.纹理.法线和材质等等,可以由我们手动编写,也可以 ...
- Goland软件使用教程(二)
Goland软件使用教程(二)一.编码辅助功能 1. 智能补全 IDE通过自动补全语句来帮助您来编写代码.快捷键“Ctrl+shift+空格”将会给你一个在当前上下文中最相关符号的列表,当您 ...
- yum 软件管理器
yum软件管理器 yum是一个强大的软件包管理器,能够自动解决安装时rpm包之间的依赖关系. 一.使用yum管理软件包 1.使用命令 yum help 查看使用方法 [root@majinhai ~] ...
- JMeter学习-009-JMeter 后置处理器实例之 - 正则表达式提取器(二)多参数获取
前文简述了通过后置处理器 - 正则表达式提取器 获取 HTTP请求 响应结果中的特定数据,未看过的亲,敬请参阅 JMeter学习-008-JMeter 后置处理器实例之 - 正则表达式提取器(一). ...
- XC软件管理器应用
这是一个基于android 4.4开发的android应用-XC软件管理器.包含应用的信息查看,打开应用以及应用的卸载等功能.非常实用的一个应用,欢迎大家下载使用. 下载地址:http://downl ...
- 基于FFMPEG的跨平台播放器实现(二)
基于FFMPEG的跨平台播放器实现(二) 上一节讲到了在Android平台下采用FFmpeg+surface组合打造播放器的方法,这一节讲一下Windows平台FFmpeg + D3D.Linux平台 ...
- Linux软件管理器(如何使用软件管理器来管理软件)
我们的Linux系统大部分都是某个Linux厂商的系统,所以这些厂商可以编译好一些软件来提供用户下载,用户下载完了之后就可以直接安装,从而省去了编译源码及其过程中的各种问题.这时我们就可以使用相应的软 ...
- GIS基础软件及操作(十二)
原文 GIS基础软件及操作(十二) 练习十二. ArcMap制图-地图版面设计 设置地图符号-各种渲染方式的使用 使用ArcMap Layout(布局)界面制作专题地图 将各种地图元素添加到地图版面中 ...
- 我终于弄懂了Python的装饰器(二)
此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 二 ...
随机推荐
- 【读书笔记】segment routing mpls数据平面-1
- [原创] debian 9.3 搭建Jira+Confluence+Bitbucket+crowd+seafile (零) 修改端口的问题
[原创] debian 9.3 搭建Jira+Confluence+Bitbucket+seafile (零) 修改端口的问题 来来来,今天说个没有人说过的事, 搭建好Jira+Confluenc ...
- 使用netty HashedWheelTimer构建简单延迟队列
背景 最近项目中有个业务,需要对用户新增任务到期后进行业务处理.使用定时任务定时扫描过期时间,浪费资源,且不实时.只能使用延时队列处理. DelayQueue 第一想到的是java自带的延时队列del ...
- 优化Android Studio/Gradle构建(转)
参考:http://hm.itheima.com/thread-204217-1-1.html
- DVWA中low级的sql注入漏洞的简单复现
第一次成功复现一个简单漏洞,于是写下这篇随笔记录一下 首先我们来看dvwa中low级的sql注入的源码 源码文件路径如下图: 源码如下: <?php if(isset($_GET['Submit ...
- 重启虚拟机后dhclient进程未运行解决办法
问题分析 重启虚拟机后,dhclient进程未运行的根因通常为: 1.NetworkManager未开启自启动导致的dhclient进程未运行 2.网卡设置未纳入NetworkManager管理导致的 ...
- OO_多线程电梯_单元总结
概述: 面向对象的第二单元是多线程电梯.第一次实现一部傻瓜电梯,每次只送一个人:第二次实现一部可稍带电梯:第三次实现三部可稍带电梯. 一.设计策略 1.第5.6次作业设计思路 第5.6次作业的架构相似 ...
- 七牛存储qshell工具
---恢复内容开始--- 工具地址:https://developer.qiniu.com/kodo/tools/1302/qshell 下载完成后:根据自己的系统选择需要的可执行文件,支持linux ...
- ARM指令集详解
一.跳转指令 B: 跳转指令 BL: 带返回的跳转指令 BLX: 带返回和状态切换的跳转指令 BX: 带状态切换的跳转指令 二.数据处理指令 1.MOV:数据传送指令 MOV{条件}{S} 目的 ...
- CentOS如何手动增加 删除swap区
SWAP是Linux中的虚拟内存,用于扩充物理内存不足而用来存储临时数据存在的.它类似于Windows中的虚拟内存.在Windows中,只可以使用文件来当作虚拟内存.而linux可以文件或者分区来当作 ...