whoimi

A geek blog

View on GitHub

Shader开发说明

文件结构

CoreRPLibrary/ShaderLibrary:

保存了大量的工具函数:光照计算工具函数,随机数计算,矩阵工具,坐标转换工具,风场,ParallaxOcclusionMapping等。

CoreRPLibrary/ShaderLibrary/API:

保存了跨平台函数的定义。

HDRP/Runtime/Material:

保存了HDRP中默认支持的材质Shader:Lit,LayeredLit,Stacklit等,都是和各自材质相关的计算。不同的材质中包含了不同的BSDF函数的实现、不同的BuiltinData的组织方式。

HDRP/Runtime/PostProcessing/Shaders:

后处理的Shader,HDRP中后处理全部使用ComputeShader。

HDRP/Runtime/RenderPipeline/ShaderPass:

ShaderPass的定义:包括了Vertex和Fragment程序的定义。

HDRP/Runtime/RenderPipeline/ShaderLibrary:

从C#当中设置的Shader参数,包括:各种变换矩阵、获取矩阵的函数、摄像机参数、场景参数、全部buffer、全局纹理、shader控制参数等。

Shader Pass

RenderPiple的代码中,在不同的时机会渲染不同的ShaderPass(通过lightmode区分)。如果要看更详细的Pass绘制时机,以及Pass之间如何组合成正确的Shader,需要看Pipeline的代码,如果随意组合会得到无法预知的结果,主要的Pass如下:

Forward

前向渲染物体使用这个Pass,正常情况下使用Deferred。透明物体可以使用Forward Pass, StackLit也是Forward Pass。

ForwardOnly

ForwardOnly的用处是:渲染透明物体或在Deferred模式下强制使用Forward模式渲染不透明物体。 例如:StackLit就是使用了{ForwardOnly和DepthForwardOnly}组合的Shader。

DepthForwardOnly

和ForwardOnly对应使用。

DepthOnly

渲染深度,Forward和Deferred必须有一个深度,使用这个Pass。

TransparentDepthPrepass

透明物体Prepass深度

TransparentDepthPostpass

透明物体Postpass深度

注意:上面所有的Pass需要正确的组合在一个Shader当中,不然会出错。例如:自己写的Shader中可以包括:{ForwardOnly,DepthForwardOnly}。 如果出现:{ForwardOnly,DepthOnly}就会出现不可预知的结果。

ShadowCaster

用于渲染阴影的Pass,和老版本不同的是:HDRP的阴影和深度使用了两个不同的Pass。

DistortionVectors

扭曲向量

DistortionVectors

屏幕运动向量

Shader编写

定义LightMode和ShaderPass

如果要实现自己的Shader,第一步需要正确的定义ShaderPass:

不透明物体:主要需要基本的光照渲染Forward或者Deferred、阴影ShadowCaster,深度DepthOnly:

Shader "HDRP"
{

    SubShader
    {
        Pass
        {
            Name "Forward"
            Tags{ "LightMode" = "Forward" }
        }
         Pass
        {
            Name "Deferred"
            Tags{ "LightMode" = "Deferred" }
        }
        Pass
        {
            Name "DepthOnly"
            Tags{ "LightMode" = "DepthOnly" }
        }
        Pass
        {
            Name "ShadowCaster"
            Tags{ "LightMode" = "ShadowCaster" }
        }
    }
}

不透明物体也可以是(类似StackLit方式):

Shader "HDRP"
{
    SubShader
    {
        Pass
        {
            Name "ForwardOnly"
            Tags{ "LightMode" = "ForwardOnly" }
        }
        Pass
        {
            Name "DepthForwardOnly"
            Tags{ "LightMode" = "DepthForwardOnly" }
            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            ENDHLSL
        }
    }
}

透明物体,需要基本的光照Pass(也可以不计算光照)ForwardOnly或者Forward,深度TransparentDepthPrepass、TransparentDepthPostpass

Shader "HDRP"
{

    SubShader
    {
        Pass
        {
            Name "ForwardOnly"
            Tags{ "LightMode" = "Forward" }
            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            ENDHLSL
        }
        Pass
        {
            Name "TransparentDepthPrepass"
            Tags{ "LightMode" = "TransparentDepthPrepass" }
        }
        Pass
        {
            Name "TransparentDepthPostpass"
            Tags{ "LightMode" = "TransparentDepthPostpass" }
        }
    }
}

可以根据自己的需求组合不同的Pass达到不同的效果。例如:一个物体如果不想要阴影,可以直接去掉ShadowCaster Pass,这样对深度没有任何影响。还有一个重要的问题就是:所有ShaderGraph或者HDRP内置的Shader都包括了ShadowCaster Pass和Depth Pass。所以,需要之后应该需要我们根据需求手动去除这个Pass。

设置Queue

控制渲染时机的除了LightMode之外,另一个是Queue。

HDRP的Queue和原本不同,现在只能定义到SubShader级别.

Shader ""
{
     SubShader
     {
        Tags { "Queue" = "Transparent" }
        Pass
        {
            // rest of the shader body...
        }
    }
}

HDRP使用了Priority定义Queue,具体参考HDMaterialTags.cs文件。

完成了Queue和LightMode的设置就算是完成一个完整的Shader设置。

Material参数

HDRP的自定义参数设置和原始版本的使用方式是一样的。

不同的是内置参数。HDRP的内置参数全部通过可查看的C#代码设置。需要包括以下两个文件。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

这两个文件包括了:版本相关的变量关键字、摄像机矩阵、场景参数、摄像机参数等内容。矩阵类型和原本也有所差异。

灯光设置

HDRP和原始版本最大的区别就是灯光计算,HDPR在一个Pass当中会计算完所有的灯光(包括区域光、环境光、雾、LightMap、LightProbe)。需要从LightList当中读取各种信息。

下面是读取灯光信息的基本形式:

#include "CustomLighting.hlsl"

void LightLoop(...)
{

    float NdotV = dot(bsdfData.normalWS, V);
    LightLoopContext context;
    context.shadowContext    = InitShadowContext();
    // 计算基于屏幕的细节阴影
    context.contactShadow    = InitContactShadow(posInput);
    context.shadowValue      = 1;
    context.sampleReflection = 0;

    uint lightCount, lightStart;

    // ===================  读取点光、聚光灯  ===================== 
    /*
    struct LightData
    {
        float3 positionRWS;
        uint lightLayers;
        float lightDimmer;
        float volumetricLightDimmer;
        float angleScale;
        float angleOffset;
        float3 forward;
        int lightType;
        float3 right;
        float range;
        float3 up;
        float rangeAttenuationScale;
        float3 color;
        float rangeAttenuationBias;
        int cookieIndex;
        int tileCookie;
        int shadowIndex;
        int contactShadowMask;
        int rayTracedAreaShadowIndex;
        float shadowDimmer;
        float volumetricShadowDimmer;
        int nonLightMappedOnly;
        float minRoughness;
        float4 shadowMaskSelector;
        float2 size;
        float diffuseDimmer;
        float specularDimmer;
    };
    */
    /*
    根据世界坐标位置和灯光类型,返回灯光索引的开始索引和数量.
    */
    GetCountAndStart(posInput, LIGHTCATEGORY_PUNCTUAL, lightStart, lightCount);
    uint i = 0; 
    for (i = 0; i < lightCount; ++i)
    {
        LightData lightData = FetchLight(lightStart, i);
        // 计算光照
    }
    
    // ===================  读取直线光  =====================
    /*
    _DirectionalLightCount:直线光个数
    _DirectionalLightDatas:所有直线光数组.
    直线光保存在CB当中,不参与FPTL,所以直接通过数组计算。
    _DirectionalLightDatas的数据结构:
    struct DirectionalLightData
    {
        float3 positionRWS;
        uint lightLayers;
        float lightDimmer;
        float volumetricLightDimmer;
        float angleScale;
        float angleOffset;
        float3 forward;
        int cookieIndex;
        float3 right;
        int tileCookie;
        float3 up;
        int shadowIndex;
        float3 color;
        int contactShadowMask;
        float shadowDimmer;
        float volumetricShadowDimmer;
        int nonLightMappedOnly;
        float minRoughness;
        float4 shadowMaskSelector;
        float diffuseDimmer;
        float specularDimmer;
    };
    */
    uint i = 0; 
    for (i = 0; i < _DirectionalLightCount; ++i)
    {
        _DirectionalLightDatas[i];
    }
    
    
    // ====================== 读取区域光 =====================
    /*
   	LightData定义同读取点光、聚光灯。
    */
    /*
     在Lit当中Loop当中可以看到用了两个while,主要是为了GPU优化。
    */
    GetCountAndStart(posInput, LIGHTCATEGORY_AREA, lightStart, lightCount);
    for(i = 0; i < lightCount; i++)
    {
        LightData lightData = FetchLight(lightStart, i);
       	// 计算光照
    }
    
    // ==================== 读取环境光,主要处理反射和折射光的分量 =====================
    /*
    这里有个重要的内容,就是反射和折射信息和光照计算不同,不是叠加,而是反射的上限应该是1。所以具体如何处理不同的反射和折射的关系需要参考Lit。
    */
    /*
    struct EnvLightData
    {
        uint lightLayers;
        float3 capturePositionRWS;
        int influenceShapeType;
        float3 proxyExtents;
        float minProjectionDistance;
        float3 proxyPositionRWS;
        float3 proxyForward;
        float3 proxyUp;
        float3 proxyRight;
        float3 influencePositionRWS;
        float3 influenceForward;
        float3 influenceUp;
        float3 influenceRight;
        float3 influenceExtents;
        float unused00;
        float3 blendDistancePositive;
        float3 blendDistanceNegative;
        float3 blendNormalDistancePositive;
        float3 blendNormalDistanceNegative;
        float3 boxSideFadePositive;
        float3 boxSideFadeNegative;
        float weight;
        float multiplier;
        int envIndex;
    };
    */
    //Lit当中首先定义了反射和折射的成分,初始化为0.
    float reflectionHierarchyWeight = 0.0; // Max: 1.0
    float refractionHierarchyWeight = _EnableSSRefraction ? 0.0 : 1.0; // Max: 1.0
    // 读取环境光照信息
    GetCountAndStart(posInput, LIGHTCATEGORY_ENV, envLightStart, envLightCount);
    
    // 先计算屏幕空间反射:叠加reflectionHierarchyWeight。这部分信息不再Tile当中
    {
        IndirectLighting indirect = EvaluateBSDF_ScreenSpaceReflection(posInput, preLightData, bsdfData,                                     reflectionHierarchyWeight);
        AccumulateIndirectLighting(indirect, aggregateLighting);
    }
    
    // 计算屏幕折射。
    if ((featureFlags & LIGHTFEATUREFLAGS_SSREFRACTION) && (_EnableSSRefraction > 0))
    {
        // 折射信息
        envLightData = FetchEnvLight(envLightStart, 0);
    }
        
    // 反射和折射探针。
    if (featureFlags & LIGHTFEATUREFLAGS_ENV)
    {
        for(i = 0;i<envLightCount;i++)
        {
            uint v_envLightIdx = FetchIndex(envLightStart, i);
            EnvLightData s_envLightData = FetchEnvLight(v_envLightIdx);    
   			// 处理环境光
        }

        // 使用天空纹理计算IBL
        if ((featureFlags & LIGHTFEATUREFLAGS_SKY) && _EnvLightSkyEnabled)
        {
            context.sampleReflection = SINGLE_PASS_CONTEXT_SAMPLE_SKY;
            EnvLightData envLightSky = InitSkyEnvLightData(0);
			// 处理envLightSky 
        }
    }
}

重要函数

需要通过Tile读取的灯光

#define LIGHTCATEGORY_PUNCTUAL (0) #define LIGHTCATEGORY_AREA (1) #define LIGHTCATEGORY_ENV (2)

FetchLight

灯光就是直接从Buffer当中读取的,但是索引是要通过FPTL、Cluster、Big-Tile等方法读取的,下面是直接读取灯光信息:

LightData FetchLight(uint start, uint i)
{
    uint j = FetchIndex(start, i);

    return _LightDatas[j];
}

LightData FetchLight(uint index)
{
    return _LightDatas[index];
}

EnvLightData FetchEnvLight(uint start, uint i)
{
    int j = FetchIndex(start, i);

    return _EnvLightDatas[j];
}

EnvLightData FetchEnvLight(uint index)
{
    return _EnvLightDatas[index];
}
FetchIndex

具体的索引如何读取,就要看是否使用FPTL、Cluster、Big-Tile或者不用光照优化策略:

#ifdef USE_FPTL_LIGHTLIST    // 使用FPTL
uint FetchIndex(uint tileOffset, uint lightOffset)
{
    const uint lightOffsetPlusOne = lightOffset + 1; // Add +1 as first slot is reserved to store number of light
    // Light index are store on 16bit
    return (g_vLightListGlobal[DWORD_PER_TILE * tileOffset + (lightOffsetPlusOne >> 1)] >> ((lightOffsetPlusOne & 1) * DWORD_PER_TILE)) & 0xffff;
}

#elif defined(USE_CLUSTERED_LIGHTLIST) // 使用Cluster  例如:对于透明物体就用Cluster

uint FetchIndex(uint lightStart, uint lightOffset)
{
    return g_vLightListGlobal[lightStart + lightOffset];
}

#elif defined(USE_BIG_TILE_LIGHTLIST) // 使用BigTile

uint FetchIndex(uint lightStart, uint lightOffset)
{
    return g_vBigTileLightList[lightStart + lightOffset];
}

#else             // 没有
// Fallback case (mainly for raytracing right now)
uint FetchIndex(uint lightStart, uint lightOffset)
{
    return 0;
}
#endif // USE_FPTL_LIGHTLIST
GetCountAndStart

获取灯光列表的起始索引和数量,同上。具体查看LightLoopDef.hlsl源文件。