最基础的几种光照原理及实现
法线变换
由于要实现光照了,法线也要变换到世界坐标里来做运算,但是法线是不能直接乘一个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
具体实现在代码里,简而言之相比之前的代码,这个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两个应用加入光照后可以得到如下渲染结果