上一节笔记地址
鼠标键盘输入我暂时还没打算看,RawInput,DirectInput,XInput和Windows消息可以用的办法太多,技术又发展的太快,等后面学会了DX12时代的键盘输入再覆盖回来就好了。反正键鼠的控制万年不变,玩不出什么花样,所以不打算钻进去。
光照!感觉这是图形学里最难的地方之一了o(︶︿︶)o !看来这篇学习笔记要写很久,希望我能多搞清楚一些原理。
法线向量
法线向量表示垂直于一个平面的一个向量,可以通过两个平面上的向量的叉积得到。光照中需要通过法向量来计算光线。法线的计算方法非常简单,通过三角形的顶点坐标,做减法得到两个向量,向量叉乘即可得到法向量,也可以提前手动指定。
前面第二节提到过model to world的变换。事实上在发生缩放变换的过程中,法线也需要变换,因为不按照xyz同比例的缩放会导致法线方向改变,所以要重新计算法向量。
计算方法:
// 顶点着色器
VertexOut VS(VertexIn vIn)
{
VertexOut vOut;
matrix viewProj = mul(g_View, g_Proj);
float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);
vOut.PosH = mul(posW, viewProj);
vOut.PosW = posW.xyz;
vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
vOut.Color = vIn.Color; // 这里alpha通道的值默认为1.0
return vOut;
}
//主要关注顶点着色器中法线的变换
定义材质
计算光强度的变化必然要引入材质的概念,在本节中,材质由如下定义:
// 物体表面材质
struct Material
{
float4 Ambient;
float4 Diffuse;
float4 Specular; // w = SpecPower
float4 Reflect;
}; // 着色器定义
struct Material
{
DirectX::XMFLOAT4 ambient;
DirectX::XMFLOAT4 diffuse;
DirectX::XMFLOAT4 specular; // w = 镜面高光指数
DirectX::XMFLOAT4 Reflect;
}; // 输入装配阶段的定义
后面的计算会用到前三个属性,这里不讨论Reflect成员变量,这个变量会在以后模拟镜子时用到。我们在镜面高光指数p放置在材质高光颜色的第4个分量中。这是因为光照不需要alpha分量,所以空出的这个位置可以储存一些有用的东西。漫反射材质的alpha分量在后面的章节中可用于alpha混合。
不同的物体拥有同种或不同不同的材质。在unity中,材质随处可见,为了优化尽量让看起来类似的物体使用同一种材质。
Phong光照模型
最终光 = 漫反射 + 环境光 + 镜面高光
属于局部光照模型,支持点光源和方向光源,只计算直接光照,光线从光源发出,经过一次反射进入摄像机,不考虑光线的遮挡。
环境光 ambient
这个最简单,所谓环境光就是一个保存起来的数。环境光用来近似模拟经过多次反射进入摄像机的光线,但实际上并没有任何光源。环境光对物体的照射与观察者的位置无关。
环境光等于:
漫反射环境光系数 点乘 环境光颜色
Lambert漫反射光 diffuse
计算漫反射光会用到兰伯特余弦定理。
兰伯特余弦定理
光线角度与照射面接收光照的值的关系。
可以非常直观的看出,
L(入射光向量的反方向) 点乘 n(法线单位向量)
即是光照强度在n上的投影,也就是dA上单位像素收到的平行光的数值。
我们将漫反射光的计算过程分为两个部分。在第一部分中,我们指定一个入射光颜色和一个漫反射材质系数。漫反射材质指定了表面所能反射和吸收的漫反射光的总量;它使用分量颜色乘法来实现。例如,表面反射50%的红光、100%的绿和 75%的蓝光,入射光是强度为80%的白光。那么,入射光颜色ld = (0.8, 0.8, 0.8),漫反射材质系数md = (0.5, 1.0, 0.75);反射光的总量为:
D =ld ⨂ md = (0.8, 0.8, 0.8)⨂(0.5, 1.0, 0.75) = (0.4, 0.8, 0.6),反射光即从材质表面反射到其他方向的光(一部分被摄像机接收)。不需要分许几何关系。
再考虑到反射光的角度,那反射光的结果就是
漫反射材质系数 点乘 (入射光反方向 点乘 法向量) 点乘 入射光颜色
镜面高光 specular
镜面高光的结果:
镜面高光材质系数 点乘 (出射光向量 点乘 顶点或像素到摄像机向量)^高光系数 点乘 入射光颜色
注意:出射光向量我们一般是不知道的,需要计算,公式:
直接用hlsl自带的reflect函数,直接通过入射向量和法线参数求出射向量。
reflect(Direction, pIn.NormalW)
高光系数决定了镜面反射区域的大小。
混合
一些疑惑
逐顶点光照 or 逐像素光照
如果是逐顶点光照(Gouraud着色),那么使用的必须是顶点法线,顶点法线有两种计算方法:①将三角形面的法线赋值给三个顶点②求出一个顶点所共享的所有三角形的法线,取平均值,赋给该顶点。逐顶点光照在顶点着色器中计算,最后像素会根据顶点颜色进行插值。顶点越少,与像素着色器的差异越明显。但是逐顶点光照非常节省计算量,所以适合给移动设备使用。
如果是逐像素光照(Phong着色),那么使用的必须是面法线,面法线通过边矢量叉积得到,可以提前计算好与颜色位置等信息一起打包。逐像素光照在像素着色器中计算,当计算某个像素时,该像素上的法线通过顶点法线插值得到。
透视校正差值
我感觉这是个很重要的部分,事实上学习笔记第一节中有关三角形颜色插值的部分我就有些迷糊,不知道底层是怎么回事,当时我还对图形学完全淡忘了,所以不妨在这时候再好好查一查。
复习一遍顶点属性插值的描述:
我们通过指定三角形的3个顶点来定义一个三角形。除位置外,顶点还可以包含其他属性,比如颜色、法线向量和纹理坐标。在视口变换之后,这些属性必须为三角形表面上的每个像素进行插值。顶点深度值也必须进行插值,以使每个像素都有一个可用于深度缓存算法的深度值。对屏幕空间中的顶点属性进行插值,其实就是对3D空间中的三角形表面进行线性插值;这一工作需要借助所谓的透视矫正插值(perspective correct interpolation)来实现。本质上,三角形表面内部的像素颜色都是通过顶点插值得到的。
我们不必关心透视精确插值的数学细节,因为硬件会自动完成这一工作;
顶点属性插值发生在光栅化阶段,在像素着色器调用之前,
二维线性插值,非常简单,如图。
v4由v1和v2插值得到,v1和v3插值得到v5,v4和v5插值再得到v6。v6即可逐行扫描三角形内部。
但是换到三维空间,就麻烦了许多,因为透视的关系,插值不再满足线性,屏幕空间的距离比例和世界空间中的距离比例不同,所以就要根据满足线性的属性来推导不满足线性的属性间的比例关系。
HLSL常量缓冲区打包原则
参考HLSL常量缓冲区打包原则
看完就知道为什么要小心打包顺序,检查一下我们顶点缓冲区和常量缓冲区的代码。
顶点缓冲区
// cpp
struct VertexPosNormalColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT4 color;
}
//vs
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float4 Color : COLOR;
};
vs常量缓冲区
// cpp
struct VSConstantBuffer
{
DirectX::XMMATRIX world;
DirectX::XMMATRIX view;
DirectX::XMMATRIX proj;
DirectX::XMMATRIX worldInvTranspose;
};
// vs
cbuffer VSConstantBuffer : register(b0)
{
matrix g_World;
matrix g_View;
matrix g_Proj;
matrix g_WorldInvTranspose;
}
ps常量缓冲区
// cpp
struct PSConstantBuffer
{
DirectionalLight dirLight;
PointLight pointLight;
SpotLight spotLight;
Material material;
DirectX::XMFLOAT4 eyePos;
};
//ps
cbuffer PSConstantBuffer : register(b1)
{
DirectionalLight g_DirLight;
PointLight g_PointLight;
SpotLight g_SpotLight;
Material g_Material;
float3 g_EyePosW;
float g_Pad;
}
可以看到vs和ps函数上声明的寄存器不同,b0和b1代表不同的区域,绑定vs和ps常量缓冲区代码
// ******************
//