上一节笔记地址 鼠标键盘输入我暂时还没打算看,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中,材质随处可见,为了优化尽量让看起来类似的物体使用同一种材质。
最终光 = 漫反射 + 环境光 + 镜面高光 属于局部光照模型,支持点光源和方向光源,只计算直接光照,光线从光源发出,经过一次反射进入摄像机,不考虑光线的遮挡。
这个最简单,所谓环境光就是一个保存起来的数。环境光用来近似模拟经过多次反射进入摄像机的光线,但实际上并没有任何光源。环境光对物体的照射与观察者的位置无关。 环境光等于: 漫反射环境光系数 点乘 环境光颜色
计算漫反射光会用到兰伯特余弦定理。 兰伯特余弦定理 光线角度与照射面接收光照的值的关系。 可以非常直观的看出, 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),反射光即从材质表面反射到其他方向的光(一部分被摄像机接收)。不需要分许几何关系。 再考虑到反射光的角度,那反射光的结果就是 漫反射材质系数 点乘 (入射光反方向 点乘 法向量) 点乘 入射光颜色
镜面高光的结果: 镜面高光材质系数 点乘 (出射光向量 点乘 顶点或像素到摄像机向量)^高光系数 点乘 入射光颜色 注意:出射光向量我们一般是不知道的,需要计算,公式: 直接用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常量缓冲区打包原则 看完就知道为什么要小心打包顺序,检查一下我们顶点缓冲区和常量缓冲区的代码。 顶点缓冲区
// 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常量缓冲区代码
// ****************** // 设置常量缓冲区描述 // D3D11_BUFFER_DESC cbd; ZeroMemory(&cbd, sizeof(cbd)); cbd.Usage = D3D11_USAGE_DYNAMIC; cbd.ByteWidth = sizeof(VSConstantBuffer); cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // 新建用于VS和PS的常量缓冲区 HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[0].GetAddressOf())); cbd.ByteWidth = sizeof(PSConstantBuffer); HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[1].GetAddressOf())); ...... ......//填充数据 //绑定到管线 // VS常量缓冲区对应HLSL寄存于b0的常量缓冲区 m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffers[0].GetAddressOf()); // PS常量缓冲区对应HLSL寄存于b1的常量缓冲区 m_pd3dImmediateContext->PSSetConstantBuffers(1, 1, m_pConstantBuffers[1].GetAddressOf());有关ps中三个光源类型在下面讨论。
平行光就表示为 环境光+漫反射光+镜面高光(即前面的Phong模型) 上面的公式是数学模型,为了代码计算应该再加一些修饰 光照方向是确定的一个方向。 着色器代码,确定了着色器代码,那dx代码就好说了 这段代码是源码的简化版本
// 像素着色器 float4 PS(VertexOut pIn) : SV_Target { // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 顶点指向眼睛的向量 float3 toEyeW = normalize(g_EyePosW - pIn.PosW); // 初始化为0 float4 ambient, diffuse, spec; ambient = g_Material.Ambient * g_DirLight.Ambient; diffuse = g_Material.Diffuse * g_DirLight.Diffuse * max(dot(-g_DirLight.Direction, pIn.NormalW),0.0f); spec = g_Material.Specular * pow(max(dot(reflect(g_DirLight.Direction, pIn.NormalW), toEyeW), 0.0f), g_Material.Specular.w) * g_DirLight.Specular; float4 litColor = pIn.Color * (ambient + diffuse + spec);//三个光照颜色全按像素颜色来 litColor.a = g_Material.Diffuse.a * pIn.Color.a; return litColor; } 标准化像素的插值后的法向量得到像素到摄像机的向量计算三个光照叠加计算方法有了,下面反推cpp代码如何绑定缓冲区就简单了,像素着色器中会用到的变量有 常量缓冲区
cbuffer PSConstantBuffer : register(b1) { DirectionalLight g_DirLight; Material g_Material; float3 g_EyePosW; float g_Pad;//用来添加额外数据保证浮点数对齐 } 物体材质(ADS)和光源属性 // hlsl struct Material { float4 Ambient; float4 Diffuse; float4 Specular; // w = SpecPower float4 Reflect; }; //cpp // 方向光 m_DirLight.ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f); m_DirLight.diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f); m_DirLight.specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f); m_DirLight.direction = XMFLOAT3(-0.577f, -0.577f, 0.577f); // 初始化用于PS的常量缓冲区的值 m_PSConstantBuffer.material.ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f); m_PSConstantBuffer.material.diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f); m_PSConstantBuffer.material.specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 5.0f); // 使用默认平行光 m_PSConstantBuffer.dirLight = m_DirLight; // 注意不要忘记设置此处的观察位置 m_PSConstantBuffer.eyePos = XMFLOAT4(0.0f, 0.0f, -5.0f, 0.0f); // 更新PS常量缓冲区资源 D3D11_MAPPED_SUBRESOURCE mappedData; HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData)); memcpy_s(mappedData.pData, sizeof(PSConstantBuffer), &m_VSConstantBuffer, sizeof(PSConstantBuffer)); m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0); 顶点属性(坐标法线和颜色) // 初始化用于VS的常量缓冲区的值 m_VSConstantBuffer.world = XMMatrixIdentity(); m_VSConstantBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH( XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f), XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f), XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f) )); m_VSConstantBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f)); m_VSConstantBuffer.worldInvTranspose = XMMatrixIdentity();//World单位矩阵用于法线变换还是单位矩阵顶点输出到像素的部分多了一个法线和世界坐标PosW,之前顶点属性有关坐标的部分只有一个PosH,阅读顶点着色器源码就能发现。
// 顶点着色器 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; }posW是模型坐标变换到世界空间的坐标,用来计算光照,而posH是模型坐标经过MVP变换后,表示在裁剪空间上的位置。
点光源跟平行光原理变化不大。 平行光的光是无衰减的,方向固定。而点光源的光照方向指向四面八方,且有衰减,这就要求在像素着色器中要按照点光源位置到像素为光照方向,计算光源到像素的向量的模,通过模来设计光照衰减方程,然后将漫反射光和镜面高光的叠加除以衰减因子。 衰减方程可以是一次,也可以是二次,主要是影响光照强度随距离的衰减速度,一般来说,两个都可以。 d表示光源与像素的距离,ai表示衰减方程的常数部分,随便写,看起来适合就可以。《3D游戏编程大师技巧》的作者表示不包含二次项的更简单的光源模型也很好使~,在代码中用向量保存。 源码简化后的点光源着色器代码
// 像素着色器 float4 PS(VertexOut pIn) : SV_Target { pIn.NormalW = normalize(pIn.NormalW); // 像素指向眼睛的向量 float3 toEyeW = normalize(g_EyePosW - pIn.PosW); // 像素到光源的向量 float3 toLight = g_PointLight.Position - pIn.PosW; float d = length(toLight); toLight /= d; // 初始化为0 float4 ambient, diffuse, spec; ambient = diffuse = spec = float4(0.0f, 0.0f, 0.0f, 0.0f); ambient = g_Material.Ambient * g_PointLight.Ambient; if (d < g_PointLight.Range) { float att = 1 / dot(g_PointLight.Att, float3(1.0f, d, d * d)); //衰减因子 diffuse = g_Material.Diffuse * g_PointLight.Diffuse * max(dot(toLight,pIn.NormalW),0.0f) * att; spec = g_Material.Specular * g_PointLight.Specular * max(pow(dot(reflect(-toLight, pIn.NormalW), toEyeW), g_Material.Specular.w), 0.0f) * att; } float4 litColor = pIn.Color * (ambient + diffuse + spec); litColor.a = g_Material.Diffuse.a * pIn.Color.a; return litColor; }hlsl数据绑定
// 点光 struct PointLight { float4 Ambient; float4 Diffuse; float4 Specular; float3 Position; float Range; float3 Att; float Pad; };cpp里绑定数据的部分跟平行光类似,就不再细看了。
是个圆锥体,自然光照带有一定的方向,光照强度只与cos角有关,按照计算镜面高光的幂指数办法如法炮制一个聚光灯因子 有些参数很容易误会,图中d表示聚光灯朝向L表示像素到光源。s越大,光照衰减越快,范围越小,可以用来间接的控制角度范围。 相当于是点光源的裁剪版本。 着色器代码
// 像素着色器 float4 PS(VertexOut pIn) : SV_Target { // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 顶点指向眼睛的向量 float3 toEyeW = normalize(g_EyePosW - pIn.PosW); float3 toLight = g_SpotLight.Position - pIn.PosW; float d = length(toLight); toLight = toLight / d; // 初始化为0 float4 ambient, diffuse, spec; ambient = diffuse = spec = float4(0.0f, 0.0f, 0.0f, 0.0f); ambient = g_Material.Ambient * g_SpotLight.Ambient; if (d < g_SpotLight.Range) { float spotLightFactor = pow(max(dot(-toLight, g_SpotLight.Direction), 0.0f), g_SpotLight.Spot); float att = 1 / dot(g_SpotLight.Att, float3(1.0f, d, d * d)); ambient *= spotLightFactor; diffuse = g_Material.Diffuse * g_SpotLight.Diffuse * dot(toLight, pIn.NormalW) * att * spotLightFactor; spec = g_Material.Specular * g_SpotLight.Specular * max(pow(dot(reflect(-toLight, pIn.NormalW), toEyeW), g_SpotLight.Specular.w), 0.0f) * att * spotLightFactor; } float4 litColor = pIn.Color * (ambient + diffuse + spec); litColor.a = g_Material.Diffuse.a * pIn.Color.a; return litColor; }hlsl数据定义
struct SpotLight { float4 Ambient; float4 Diffuse; float4 Specular; float3 Position; float Range; float3 Direction; float Spot; float3 Att; float Pad; };按照惯例,还是参考这里的博客
尝试修改本章Demo的光照,让方向光只射出红光,点光灯只射出绿光,聚光灯只射出蓝光。尝试修改本章Demo所用到的材质,让其只反射红光。尝试修改本章Demo所用到的聚光灯,通过鼠标滚轮的形式,对光照汇聚强>度增加/减少,范围为2-512,观察效果。尝试修改本章Demo所用到的材质,看看如果镜面反射强度的值小于1会发生什么情况。尝试用HLSL内建函数lit来改写LightHelper.hlsli 将光源的ADS中任意一个参数改成(0,1,0,1),把除了G以外的分量过滤掉,就出现纯绿色的光分量了。方法与1类似复习上一节更新缓冲区的方法 bool GameApp::UpdateSpot(const int spot) { D3D11_MAPPED_SUBRESOURCE mappedData; HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData)); m_PSConstantBuffer.spotLight.spot = spot; memcpy_s(mappedData.pData, sizeof(PSConstantBuffer), &m_PSConstantBuffer, sizeof(PSConstantBuffer)); m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0); return true; } //UpdateScene Mouse::State mouseState = m_pMouse->GetState(); m_MouseTracker.Update(mouseState); static int spot = 2; if (mouseState.leftButton == true) { spot = max(spot + 1, 2); spot = min(spot, 512); UpdateSpot(spot); } else if (mouseState.rightButton == true) { spot = max(spot - 1, 2); spot = min(spot, 512); UpdateSpot(spot); }没找着怎么弄滚轮。。。。 4. 参考下面两个图 根据幂的变化在图像上有两种变化 幂越大,光圈越小,光圈边缘的过渡区越长。 这两种现象分别受图中图像y趋近于0的x值大小和y趋近于0时x的导数大小影响。x值越小,光圈越小,因为x值越小,y值变化越快,所以光圈越小。而对于导数来说,导数最后趋近于无穷大,那么光圈边缘的过渡区趋近于0,那么光圈边缘处与点光源入射角为负的地方差距越明显。 5. 微软文档中有记录有关HLSL内置函数 关于lit的定义 看了半天没搞懂,谷歌了半天好像没什么人讨论,暂时放弃,以后碰到再说。
