自定义Shader

在小游戏性能解决方案中,自定义Shader采用HLSL进行编写,提供的工具链会将HLSL跨平台编译成GLSL/MSL/SPIR-V,以便在不同的环境上运行

编写第一个自定义Shader

我们先来看一组Shader

// simple3D.vertex.hlsl

// 公共头文件,包含引擎内建hlsl函数、宏和结构定义
#include <common.inc>

// 材质定义,需与.effect文件中的配置一致
cbuffer material
{
    float4 _MainTex_ST;
    float4 _Color;
}
// vertex output, 从vertex传递到fragment的数据
struct FVertexOutput
{
    float4 Position : SV_Position;
    float2 TexCoord : TEXCOORD0;
};

// vertex入口函数
void Main(in FEffect3DVertexInput In, out FVertexOutput Out)
{
    // 引擎内建顶点处理,进行Particle/Skin/Line/Trail等顶点处理
	FVertexProcessOutput VPOut;
	Effect3DVertexProcess(In, VPOut);

    Out.Position = WorldToClipPosition(VPOut.WorldPosition);
    Out.TexCoord = TRANSFER_TEXCOORD(VPOut.TexCoord, _MainTex_ST);
}
// simple3D.pixel.hlsl

#include <common.inc>
cbuffer material
{
    float4 _MainTex_ST;
    float4 _Color;
}
struct FVertexOutput
{
    float4 Position : SV_Position;
    float2 TexCoord : TEXCOORD0;
};

// texture声明,需与.effect文件中声明的一致
DECLARE_TEXTURE(_MainTex);
float4 Main(in FVertexOutput In) : SV_Target0
{
    fixed4 texColor = SAMPLE_TEXTURE(_MainTex, In.TexCoord);
    return texColor * _Color;
}

使用#include

示例中第一行

#include <common.inc>

表示引入内建的公共hlsl片段,该片段中定义了大量辅助公共宏用于辅助自定义shader的编写

内建变量

  • float3 WorldSpaceViewPosition 世界空间的相机位置
  • float3 WorldSpaceLightDir 实时平行光源在世界空间的方向
  • float3 LightColor 实时平行光源颜色
  • float3 AmbientLight 环境光颜色
  • float GameTime 游戏时间,单位为s

内建结构体

// 3D的顶点输入数据原始结构,开发者不用关心内容
struct FEffect3DVertexInput

// 3D顶点处理输出数据结构
struct FVertexProcessOutput {
    float4 Position;         // 顶点模型空间坐标(某些特效情况下不保证该属性正确)
    float4 WorldPosition;    // 顶点世界空间坐标
	float2 TexCoord;         // 顶点贴图坐标
	float4 Color;            // 顶点颜色
	float3 Normal;           // 顶点法向量
	float4 Tangent;          // 顶点切向量
	float2 LightMapCoord;    // 光照贴图坐标
}

内建函数

  • float4 ObjectToWorldPosition(float4 point):将local space position转换到world Space
  • float4 WorldToClipPosition(float4 point):将world space position 转换到 clip space
  • float2 TRANSFER_TEXCOORD(float2 TexCoord, float4 ScaleOffset) : 将UV坐标和scaleOffset进行计算,输出最终的UV坐标
  • DECLARE_TEXTURE(name): 声明贴图
  • SAMPLE_TEXTURE(name, float2 TexCoord): 贴图采样
  • Effect3DVertexProcess(in FEffect3DVertexInput In, out FVertexProcessOutput Out) : 引擎内建顶点处理,进行Particle/Skin/Line/Trail等顶点处理

也可以使用如下方式引入自定义的公共hlsl片段

// relative path
#include "./yourCommonInc.hlsl"

自定义Shader进阶

我们先来看一组示例Shader

// blinnPhong3D.vertex.hlsl

#include <common.inc>
// varying define
struct FVertexOutput
{
    float4 Position : SV_Position;
    float2 TexCoord : TEXCOORD0;
    // world space light direction
    float3 LightDir : TEXCOORD1;
    // world space view direction
    float3 ViewDir : TEXCOORD2;
    // world space normal direction
    float3 WorldNormal:TEXCOORD3;
    LIGHTMAP_COORDS(4)
    FOG_COORDS(5)
    SHADOW_COORDS(6)
};
cbuffer material
{
    float4 _Color;
    float4 _MainTex_ST;
    float _Shininess;
}
void Main(in FEffect3DVertexInput In, out FVertexOutput Out)
{
	FVertexProcessOutput VPOut;
	Effect3DVertexProcess(In, VPOut);
    Out.TexCoord = TRANSFER_TEXCOORD(VPOut.TexCoord, _MainTex_ST);

    // position
    Out.Position = WorldToClipPosition(VPOut.WorldPosition);

    float3 worldSpaceViewDir = normalize(WorldSpaceViewPosition - VPOut.WorldPosition.xyz);
    float3 worldSpaceLightDir = WorldSpaceLightDir;
    Out.WorldNormal = ObjectToWorldNormal(VPOut.Normal);
    Out.LightDir = worldSpaceLightDir;
    Out.ViewDir = worldSpaceViewDir;

    TRANSFER_LIGHTMAP(In, Out);
    TRANSFER_SHADOW(Out, VPOut.WorldPosition.xyz);
    TRANSFER_FOG(Out, VPOut.WorldPosition.xyz);
}
// blonnPhong3D.pixel.hlsl

#include <common.inc>
struct FVertexOutput
{
    float4 Position : SV_Position;
    float2 TexCoord : TEXCOORD0;
    float3 LightDir : TEXCOORD1;
    float3 ViewDir : TEXCOORD2;
    float3 WorldNormal : TEXCOORD3;
    LIGHTMAP_COORDS(4)
    FOG_COORDS(5)
    SHADOW_COORDS(6)
};
cbuffer material
{
    float4 _Color;
    float4 _MainTex_ST;
    float _Shininess;
}
DECLARE_TEXTURE(_MainTex);

// define BlinnPhong lighting function
void BlinnPhongLighting(in half3 lightColor, in half3 lightDir, in half3 normal, in half3 viewDir, in float specularFactor,
                        out float3 diffuseOut, out float3 specularOut)
{
    half3 h = normalize(viewDir + lightDir);
    half ln = max(0.0, dot(+lightDir, normal));
    float nh = max(0.0, dot(h, normal));
    diffuseOut = lightColor * ln;
    specularOut = lightColor * pow(nh, specularFactor * 128.0);
}
float4 Main(in FVertexOutput In) : SV_Target0
{
    fixed4 mainTexColor = SAMPLE_TEXTURE(_MainTex, In.TexCoord);
    fixed4 albedo = mainTexColor * _Color;

    // Blinnphong Lighting
    fixed3 diffuse = fixed3(0.0, 0.0, 0.0);
    fixed3 specular = fixed3(0.0, 0.0, 0.0);
    half3 lightDir = normalize(In.LightDir);
    half3 viewDir = normalize(In.ViewDir);
    half3 worldspaceNormal = normalize(In.WorldNormal);
    BlinnPhongLighting(LightColor, lightDir, worldspaceNormal, viewDir, _Shininess, diffuse, specular);

    // shadow attenuation
    float attenuation = SHADOW_ATTENUATION(In);


    fixed3 finalDiffuse = fixed3(0.0,0.0,0.0);
    // use lightMap or not
#if defined(USE_LIGHTMAP)
    // sample lightMapColor
    fixed3 lightMapColor = SAMPLE_LIGHTMAP(In);
    // use lightMap, mix shadow with lightMap
    finalDiffuse = MixLightmapWithRealtimeAttenuation(lightMapColor, attenuation, worldspaceNormal);
#else
    // no lightMap, mix shadow with ambientLight
    finalDiffuse = fixed3(AmbientLight) + diffuse * attenuation;
#endif
    // get outColor
    fixed4 outColor = fixed4(albedo.rgb * (finalDiffuse + specular), albedo.a);

    // apply fog to outColor
    APPLY_FOG(In, outColor);
    return outColor;
}

自定义Shader中使用蒙皮骨骼

需要在effect文件中需要蒙皮的pass中添加compileFlags项,在内建的Effect3DVertexProcess中将处理蒙皮

"compileFlags": [
  "Skin"
]

自定义Shader中使用雾效

首先需要在effect文件中需要使用雾效的pass中添加compileFlags项

"compileFlags": [
  "Fog"
]

在vertexShader的output和pixelShader的input中添加shadow texCoord声明

FOG_COORDS(<TEXCOORD INDEX>)

在vertexShader中添加如下代码计算雾效系数

// FVertexProcessOutput VPOut
TRANSFER_FOG(Out, VPOut.WorldPosition.xyz);

在pixelShader的最后添加如下代码将最终输出颜色进行雾效混合

APPLY_FOG(In, outColor);

自定义Shader中使用阴影

首先需要在effect文件中需要使用阴影的pass中添加compileFlags项

"compileFlags": [
  "Shadow"
]

在vertexShader的output和pixelShader的input中添加shadow texCoord声明

SHADOW_COORDS(n)

在vertexShader中添加如下代码计算shadow texCoord

// FVertexProcessOutput VPOut
TRANSFER_SHADOW(Out, VPOut.WorldPosition.xyz);

在pixelShader中添加如下代码计算shadow attenuation

float attenuation = SHADOW_ATTENUATION(In);

自定义Shader中使用lightMap

首先需要在effect文件中添加compileFlags项

"compileFlags": [
  "LightMap"
]

在vertexShader的output和pixelShader的input中添加lightmap texCoord声明

LIGHTMAP_COORDS(n)

在vertexShader中添加如下代码计算lightmap texCoord

TRANSFER_LIGHTMAP(In, Out);

在pixelShader中添加如下代码采样lightMapColor,并与实时阴影进行混合


// need to get shadowAttenuation like this
// float attenuation = SHADOW_ATTENUATION(In);
#if defined(USE_LIGHTMAP)
    fixed3 lightMapColor = SAMPLE_LIGHTMAP(In);
    fixed3 finalDiffuse = MixLightmapWithRealtimeAttenuation(lightMapColor, attenuation, worldspaceNormal);
#else
    fixed3 finalDiffuse = fixed3(AmbientLight) + diffuse * attenuation;
#endif