DirectX12 3D 游戏开发与实战第十章内容(下)
阅读原文时间:2023年07月10日阅读:1

仅供个人学习使用,请勿转载。谢谢!

10、混合

本章将研究混合技术,混合技术可以让我们将当前需要光栅化的像素(也称为源像素)和之前已经光栅化到后台缓冲区的像素(也称为目标像素)进行融合。因此,该技术可以用来渲染如水和玻璃之类的半透物体。

  1. 理解混合技术的工作原理,并且在Direct3D中运用该技术
  2. 学习Direct3D所支持的不同混合模式
  3. 探究如何使用alpha分量来调节图元的透明度
  4. 学会仅通过HLSL中的clip函数来组织向后台缓冲区中绘制像素

源alpha分量可以用于在RGB混合的过程中控制像素的透明度,而混合方程中的源颜色实际上来自于像素着色器。在第九章中,我们将漫反射材质中的alpha分量作为纹理着色器的alpha分量输出,因此,我们可以利用漫反射图(diffuse map)中的alpha通道来控制混合过程中的透明度了。

float4 PS(VertexOut pin) : SV_Target
{
    float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap,pin.TexC)*gDiffuseAlbedo;
    ……
       //从漫发射反照率获取alpha值的常用方法
       litColor.a = diffuseAlbedo.a;
    return litColor;
}

我们可以在常见的图像编辑软件中添加alpha通道,然后到处为支持alpha通道的格式,如DDS

有些时候,我们希望彻底禁止某一个像素参与后续的处理,这时候我们可以通过HLSL中的内置函数clip(x)实现,该函数仅供像素着色器使用,当x < 0时,该像素将会不在参与后面的处理阶段,因此用这个函数来绘制透明和非透明相见的像素是最合适的了。(比如铁丝网)

在像素着色器中,我们将会采集像素的alpha分量,如果该值极小接近于0,则表明该像素是完全透明的,那么我们将会通过clip(x)函数将该像素彻底从后续的处理阶段中移除。

float4 PS(VertexOut pin) : SV_Traget
{
    float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap,pin.TexC)*gDiffuseAlbedo;
    #ifdef ALPHA_TEXT
    //如果alpha < 0.1f,则抛弃该像素,我们要在着色器中尽早执行该操作,以尽快检测处满足条件的像素并
    //退出着色器,从而跳过后续的处理过程
    clip(diffuseAlbedo.a - 0.1f);
    #endif
    ……
       //从漫发射反照率中获取alpha值的常用手段
       litColor.a = diffuseAlbedo.a;
    return litColor;
}

通过上述的代码可以看出,只有在定义了ALPHA_TEXT宏的时候我们才会执行透明像素的筛选。因为我们有时候可以不希望对某些渲染项执行clip(x)方法,所以我们要有能力针对特殊的着色器开启或关闭clip函数的调用,而且alpha测试的开销也不小,因此我们不能一直开着。

注意:通过混合操作也可以实现相同的效果,但是我们要优先使用clip函数:

  1. 无需执行混合运算(可以禁用混合运算)
  2. 处理期间不需要考虑绘制顺序的问题
  3. 通过提前从像素着色器筛选出要抛弃的像素,可以避免该像素参与后续的处理过程

接下来我们将介绍如何实现雾化效果,实现雾化效果的流程如下:

  1. 指明雾气的颜色、由摄像机到雾气的最近距离以及雾气的分散范围
  2. 将网格三角形上点的颜色设置为原色于雾气颜色的加权平均值

\[foggedColor = litColor + s(fogColor - litColor) = (1-s)litColor + s·fogColor
\]

参数s的范围为[0 , 1]之间,由一个函数确定(该函数的参数为摄像机位置、被雾气覆盖物体表面点)。参数s的定义如下:

\[s = saturate((dist(p,E) - fogStart)/fogRange)
\]

其中saturate会将其参数限制在范围[0, 1]之间,dist(p, E)表示表面点p到摄像机位置E之间的距离。

当dist(p, E)小于fogStart的时候,雾色将不会改变物体顶点的本色,即s = 0;

\[foggedColor = litColor;
\]

当dist(p, E)大于fogEnd(fogStart + fogRange)时,雾色将会完全遮住物体,即s = 1;

\[foggedColor = fogColor;
\]

下面的代码将展示如何实现雾化效果,我们先计算距离,然后再像素层级进行插值,最后求出光照颜色。

// Default.hlsli文件

// 光照数量的默认值
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif

#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif

#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif

// 包含光照所用的结构体和函数
#include "LightingUtil.hlsli"

Texture2D    gDiffuseMap : register(t0);

SamplerState gsamPointWrap        : register(s0);
SamplerState gsamPointClamp       : register(s1);
SamplerState gsamLinearWrap       : register(s2);
SamplerState gsamLinearClamp      : register(s3);
SamplerState gsamAnisotropicWrap  : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);

// 每一帧都在变化的常量数据
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorld;
    float4x4 gTexTransform;
};

// 绘制过程中所使用的杂项常量数据
cbuffer cbPass : register(b1)
{
    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;

    // 允许应用程序在每一帧都能改变雾化效果的参数
    // 比如我们可能只在一天中的特定时间才使用雾化效果
    float4 gFogColor;
    float gFogStart;
    float gFogRange;
    float2 cbPerObjectPad2;

    Light gLights[MaxLights];
};

//每种材质中不同的常量数据
cbuffer cbMaterial : register(b2)
{
    //漫反射反照率
    float4   gDiffuseAlbedo;
    //介质的一种属性
    float3   gFresnelR0;
    //粗造程度
    float    gRoughness;
    //材质变换矩阵
    float4x4 gMatTransform;
};

struct VertexIn
{
    float3 PosL    : POSITION;
    float3 NormalL : NORMAL;
    float2 TexC    : TEXCOORD;
};

struct VertexOut
{
    float4 PosH    : SV_POSITION;
    float3 PosW    : POSITION;
    float3 NormalW : NORMAL;
    float2 TexC    : TEXCOORD;
};



// VS.hlsl文件

#include "Default.hlsli"

VertexOut VS(VertexIn vin)
{
    VertexOut vout = (VertexOut)0.0f;

    //将顶点变换到世界空间
    float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
    vout.PosW = posW.xyz;

    //假设进行的是等比变换,否则要使用世界矩阵的逆转置矩阵
    vout.NormalW = mul(vin.NormalL, (float3x3) gWorld);

    //将顶点变换到齐次裁剪空间
    vout.PosH = mul(posW, gViewProj);

    //为三角形插值输出顶点属性
    float4 texC = mul(float4(vin.TexC, 1.0f, 1.0f), gTexTransform);
    vout.TexC = mul(texC, gMatTransform).xy;

    return vout;
}


// PS.hlsl文件

#include "Default.hlsli"

float4 PS(VertexOut pin) : SV_Target
{
    float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo;

    //alpha通道测试
#ifndef ALPHA_TEXT
    clip(diffuseAlbedo.a - 0.1f);
#endif

    //对法线插值可能导致其非规范化,所以需要重新对他进行规范化处理
    pin.NormalW = normalize(pin.NormalW);

    //光线经过物体表面一点反射到观察点这一方向上的向量
    float3 toEyeW = gEyePosW - pin.PosW;
    float distToEye = length(toEyeW);
    //规范化处理
    toEyeW /= distToEye;

    float4 ambient = gAmbientLight * diffuseAlbedo;

    const float shininess = 1.0f - gRoughness;
    Material mat = { diffuseAlbedo, gFresnelR0, shininess };
    float3 shadowFactor = 1.0f;
    float4 directLight = ComputeLighting(gLights, mat, pin.PosW, pin.NormalW, toEyeW, shadowFactor);

    float4 litColor = ambient + directLight;

#ifndef FOG
    float fogAmount = saturate((distToEye - gFogStart) / gFogRange);
    litColor = lerp(litColor, gFogColor, fogAmount);
#endif

    //从漫反射反照率中获取alpha常用的手段
    litColor.a = diffuseAlbedo.a;

    return litColor;

}

在演示程序中,我们通过向CompileShader函数提供下列D3D_SHADER_MACRO结构体来开启雾化效果

const D3D_SHADER_MACRO defines[] =
{
    "FOG","1",
    NULL,NULL
};
mShadersp["opaquePS"] = d3dUtil::CompileShader(L"HLSL\\PS.hlsl",defines,"PS","ps_5_1");

演示程序运行效果:

  1. 混合是一种将档期那光栅化的像素(源像素)与之前的已经完成关光栅化的像素并存在后台缓冲区中的像素(目标像素)相互融合的技术,混合使我们可以渲染出半透明效果的物体,比如水和玻璃
  2. RGB分量和alpha分量的混合运算是各自单独展开的,目的是希望可以扩展出更多效果
  3. 混合因子是混合方程可以自定义的法宝,这些混合因子是枚举类型D3D12_BLEND中的成员之一。注意,对于alpha分量来说,不可以使用以COLOR为后缀的混合因子
  4. 源alpha的信息来自于漫发射材质,在外面自己编写的应用程序框架中,漫反射材质由纹理图来定义,而源alpha就是纹理图中的alpha通道
  5. 通过HLSL的内部函数clip(x)可以将源像素从后续的处理过程中完全屏蔽掉
  6. 可以使用雾化效果来模拟各种气象环境效果和大气透视效果,以此来掩饰远处场景渲染的失真以及物体忽然出现在视锥体之中从而闯进用户视野的情况。