由于要实现光照了,法线也要变换到世界坐标里来做运算,但是法线是不能直接乘一个world矩阵来变换的,因为如果这个world矩阵包含了各向不统一的缩放,法线就不会再垂直于表面了。
如图,我们要的是c而不是b,如果要变换后依然垂直,应该不是直接乘世界矩阵,而是乘世界矩阵的逆转置。而且有一点要注意的是,逆转置矩阵不能随便乘其他矩阵,除非把矩阵里的translation成分剔除掉,因为本来我们把向量的第四维设置成0,就是为了translation不会影响向量,只会影响点,translation在第四行,但是如果逆转置了,translation就被翻上来了,翻上来不要紧,只要规定向量第四维是0还是不会有影响,但是如果要和别的矩阵乘的话,第四列里的translation就会“泄露”到其他矩阵里去,影响结果,所以之后我们处理法向量的时候应该只要world矩阵的前3x3和法向量的前三维,即把translation剔除掉。
有两个概念可以了解一下,radiant flux辐射通量,即每秒通过的能量,irradiance辐射照度,即每秒通过单位面积的能量值。这里说的兰伯特只是指漫反射和高光部分遵循兰伯特余弦定理,实际上还有个环境光我们考虑各向强度完全相同,也就不考虑角度了。
漫反射我们认为各个角度都相同,而高光的话则要考虑菲涅尔和粗糙度的影响。
Fresnel效应的Schlick近似:
其中0度的RF我们在材质里定义。
然后还有个光滑程度的影响
这个m是光滑度,越大越光滑,在我们这个demo中,材质的shininess等于1-roughness,而m=256*shininess。然后h向量是把入射光方向L和toEye向量对半分的中间向量,即halfVec。m越大反光位置越集中,不同m的粗糙度因数曲线如下
然后最终的光照结果可以由下式求得
是环境光的rgb,
是材质的albedo,L是点到光源的单位向量,n是法线,
是光照rgb,故括号外面这个是考虑了兰伯特余弦的光照rgb,然后括号里面
的部分是漫反射,高光则是乘了菲涅尔的因数和粗糙度的因数,其中h是half vector,
则是h和L的夹角,
是已知
的情况下算出来的菲涅尔因数,
则是前面说过的粗糙度因数,m是光滑度,在这里是(1-roughness)*256。
平行光:只需要记录一个方向即可
点光源:一般的衰减是平方反比,不过这章的demo中实现的是线性衰减,要记录位置和光强
聚光灯:用 来表示聚光灯的方向,那么在点光源的基础上乘上个 得到光强
具体实现如下
#define MaxLights 16 struct Light { float3 Strength; float FalloffStart; // point/spot light only float3 Direction; // directional/spot light only float FalloffEnd; // point/spot light only float3 Position; // point light only float SpotPower; // spot light only }; struct Material { float4 DiffuseAlbedo; float3 FresnelR0; float Shininess; }; float CalcAttenuation(float d, float falloffStart, float falloffEnd) { // Linear falloff. return saturate((falloffEnd-d) / (falloffEnd - falloffStart)); } // Schlick gives an approximation to Fresnel reflectance (see pg. 233 "Real-Time Rendering 3rd Ed."). // R0 = ( (n-1)/(n+1) )^2, where n is the index of refraction. float3 SchlickFresnel(float3 R0, float3 normal, float3 lightVec) { float cosIncidentAngle = saturate(dot(normal, lightVec)); float f0 = 1.0f - cosIncidentAngle; float3 reflectPercent = R0 + (1.0f - R0)*(f0*f0*f0*f0*f0); return reflectPercent; } float3 BlinnPhong(float3 lightStrength, float3 lightVec, float3 normal, float3 toEye, Material mat) { const float m = mat.Shininess * 256.0f; float3 halfVec = normalize(toEye + lightVec); float roughnessFactor = (m + 8.0f)*pow(max(dot(halfVec, normal), 0.0f), m) / 8.0f; float3 fresnelFactor = SchlickFresnel(mat.FresnelR0, halfVec, lightVec); float3 specAlbedo = fresnelFactor*roughnessFactor; // Our spec formula goes outside [0,1] range, but we are // doing LDR rendering. So scale it down a bit. specAlbedo = specAlbedo / (specAlbedo + 1.0f); return (mat.DiffuseAlbedo.rgb + specAlbedo) * lightStrength; } //--------------------------------------------------------------------------------------- // Evaluates the lighting equation for directional lights. //--------------------------------------------------------------------------------------- float3 ComputeDirectionalLight(Light L, Material mat, float3 normal, float3 toEye) { // The light vector aims opposite the direction the light rays travel. float3 lightVec = -L.Direction; // Scale light down by Lambert's cosine law. float ndotl = max(dot(lightVec, normal), 0.0f); float3 lightStrength = L.Strength * ndotl; return BlinnPhong(lightStrength, lightVec, normal, toEye, mat); } //--------------------------------------------------------------------------------------- // Evaluates the lighting equation for point lights. //--------------------------------------------------------------------------------------- float3 ComputePointLight(Light L, Material mat, float3 pos, float3 normal, float3 toEye) { // The vector from the surface to the light. float3 lightVec = L.Position - pos; // The distance from surface to light. float d = length(lightVec); // Range test. if(d > L.FalloffEnd) return 0.0f; // Normalize the light vector. lightVec /= d; // Scale light down by Lambert's cosine law. float ndotl = max(dot(lightVec, normal), 0.0f); float3 lightStrength = L.Strength * ndotl; // Attenuate light by distance. float att = CalcAttenuation(d, L.FalloffStart, L.FalloffEnd); lightStrength *= att; return BlinnPhong(lightStrength, lightVec, normal, toEye, mat); } //--------------------------------------------------------------------------------------- // Evaluates the lighting equation for spot lights. //--------------------------------------------------------------------------------------- float3 ComputeSpotLight(Light L, Material mat, float3 pos, float3 normal, float3 toEye) { // The vector from the surface to the light. float3 lightVec = L.Position - pos; // The distance from surface to light. float d = length(lightVec); // Range test. if(d > L.FalloffEnd) return 0.0f; // Normalize the light vector. lightVec /= d; // Scale light down by Lambert's cosine law. float ndotl = max(dot(lightVec, normal), 0.0f); float3 lightStrength = L.Strength * ndotl; // Attenuate light by distance. float att = CalcAttenuation(d, L.FalloffStart, L.FalloffEnd); lightStrength *= att; // Scale by spotlight float spotFactor = pow(max(dot(-lightVec, L.Direction), 0.0f), L.SpotPower); lightStrength *= spotFactor; return BlinnPhong(lightStrength, lightVec, normal, toEye, mat); } float4 ComputeLighting(Light gLights[MaxLights], Material mat, float3 pos, float3 normal, float3 toEye, float3 shadowFactor) { float3 result = 0.0f; int i = 0; #if (NUM_DIR_LIGHTS > 0) for(i = 0; i < NUM_DIR_LIGHTS; ++i) { result += shadowFactor[i] * ComputeDirectionalLight(gLights[i], mat, normal, toEye); } #endif #if (NUM_POINT_LIGHTS > 0) for(i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS+NUM_POINT_LIGHTS; ++i) { result += ComputePointLight(gLights[i], mat, pos, normal, toEye); } #endif #if (NUM_SPOT_LIGHTS > 0) for(i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i) { result += ComputeSpotLight(gLights[i], mat, pos, normal, toEye); } #endif return float4(result, 0.0f); }
具体实现在代码里,简而言之相比之前的代码,这个demo多做了这么一些事:
定义了一个material类,把具体的实例存在mMaterials这个map里,然后root parameter加了一个root descriptor来作为material的cbv,这个cb是每个材质共用的,假如要更改材质的属性,就要把这个材质的NumFramesDirty设置成NumFrameResource,这是因为每个frame resource都有自己的材质cb,也就是如果要改材质要改掉这三个cb里的内容才行,那么就从cpu这一帧开始(比gpu快三帧),之后的三帧每次循环到传material cb的参数的时候都把NumFramesDirty减1,这样就只会更新三次(因为我们不希望频繁地更新cb的值,因为要把数据上传到gpu,设置cb的目的就在于比顶点传参快,因为上传频率少,每个物体、pass、材质可以共用,故这里我们只希望传三次对应3个FrameResource的cb)。然后update函数里要有个UpdateMaterialCBs来不断地更新材质cb。最后,渲染的时候要把material cb绑定到渲染管线,根据mat的index和byteSize来offset到cb中对应的位置然后cmd->SetGraphicsRootConstantBufferView即可,注意这里要传入的是对应cb中的部分的gpu地址,而不是cbv,root descriptor这个root parameter本身就是cbv了。
光源在pass cb中传入,根据传统,按照平行光、点光源、聚光灯的顺序传入。
shader主体部分代码如下
// Defaults for number of lights. #ifndef NUM_DIR_LIGHTS #define NUM_DIR_LIGHTS 1 #endif #ifndef NUM_POINT_LIGHTS #define NUM_POINT_LIGHTS 0 #endif #ifndef NUM_SPOT_LIGHTS #define NUM_SPOT_LIGHTS 0 #endif // Include structures and functions for lighting. #include "LightingUtil.hlsl" // Constant data that varies per frame. cbuffer cbPerObject : register(b0) { float4x4 gWorld; }; cbuffer cbMaterial : register(b1) { float4 gDiffuseAlbedo; float3 gFresnelR0; float gRoughness; float4x4 gMatTransform; }; // Constant data that varies per material. cbuffer cbPass : register(b2) { float4x4 gView; float4x4 gInvView; float4x4 gProj; float4x4 gInvProj; float4x4 gViewProj; float4x4 gInvViewProj; float3 gEyePosW; float cbPerObjectPad1; float2 gRenderTargetSize; float2 gInvRenderTargetSize; float gNearZ; float gFarZ; float gTotalTime; float gDeltaTime; float4 gAmbientLight; // Indices [0, NUM_DIR_LIGHTS) are directional lights; // indices [NUM_DIR_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHTS) are point lights; // indices [NUM_DIR_LIGHTS+NUM_POINT_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHT+NUM_SPOT_LIGHTS) // are spot lights for a maximum of MaxLights per object. Light gLights[MaxLights]; }; struct VertexIn { float3 PosL : POSITION; float3 NormalL : NORMAL; }; struct VertexOut { float4 PosH : SV_POSITION; float3 PosW : POSITION; float3 NormalW : NORMAL; }; VertexOut VS(VertexIn vin) { VertexOut vout = (VertexOut)0.0f; // Transform to world space. float4 posW = mul(float4(vin.PosL, 1.0f), gWorld); vout.PosW = posW.xyz; // Assumes nonuniform scaling; otherwise, need to use inverse-transpose of world matrix. vout.NormalW = mul(vin.NormalL, (float3x3)gWorld); // Transform to homogeneous clip space. vout.PosH = mul(posW, gViewProj); return vout; } float4 PS(VertexOut pin) : SV_Target { // Interpolating normal can unnormalize it, so renormalize it. pin.NormalW = normalize(pin.NormalW); // Vector from point being lit to eye. float3 toEyeW = normalize(gEyePosW - pin.PosW); // Indirect lighting. float4 ambient = gAmbientLight*gDiffuseAlbedo; const float shininess = 1.0f - gRoughness; Material mat = { gDiffuseAlbedo, gFresnelR0, shininess }; float3 shadowFactor = 1.0f; float4 directLight = ComputeLighting(gLights, mat, pin.PosW, pin.NormalW, toEyeW, shadowFactor); float4 litColor = ambient + directLight; // Common convention to take alpha from diffuse material. litColor.a = gDiffuseAlbedo.a; return litColor; }
将Shapes和LandAndWaves两个应用加入光照后可以得到如下渲染结果