[DirectX12学习笔记] 光照

最基础的几种光照原理及实现


法线变换

由于要实现光照了,法线也要变换到世界坐标里来做运算,但是法线是不能直接乘一个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的粗糙度因数曲线如下
在这里插入图片描述
然后最终的光照结果可以由下式求得
在这里插入图片描述
A L A_{L} 是环境光的rgb, m d m_{d} 是材质的albedo,L是点到光源的单位向量,n是法线, B L B_{L} 是光照rgb,故括号外面这个是考虑了兰伯特余弦的光照rgb,然后括号里面 m d m_{d} 的部分是漫反射,高光则是乘了菲涅尔的因数和粗糙度的因数,其中h是half vector, α h \alpha_{h} 则是h和L的夹角, R F ( α h ) R_{F}(\alpha_{h}) 是已知 R F ( 0 ) R_{F}(0^\circ) 的情况下算出来的菲涅尔因数, m + 8 8 ( n h ) \frac{m+8}{8}(\vec n\cdot \vec h) 则是前面说过的粗糙度因数,m是光滑度,在这里是(1-roughness)*256。

光照类型

平行光:只需要记录一个方向即可

点光源:一般的衰减是平方反比,不过这章的demo中实现的是线性衰减,要记录位置和光强

聚光灯:用 d d 来表示聚光灯的方向,那么在点光源的基础上乘上个 m a x ( L d , 0 ) s max(-L\cdot d,0)^s 得到光强

具体实现如下

#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两个应用加入光照后可以得到如下渲染结果

在这里插入图片描述
在这里插入图片描述