【节点】[CustomFunction节点]原理解析与实际应用

【Unity Shader Graph 使用与特效实现】专栏-直达

自定义函数节点是Unity URP Shader Graph中一个强大而灵活的工具,它允许开发者在可视化着色器编辑环境中注入自定义的HLSL代码。这个节点为那些需要更精细控制和优化的着色器效果提供了可能,填补了可视化编程与代码级控制之间的空白。

在Shader Graph的节点系统中,大多数节点都是预定义的、功能固定的,而自定义函数节点则打破了这种限制,让开发者能够实现任何通过标准节点难以或无法实现的效果。无论是复杂的数学计算、特殊的纹理处理算法,还是需要访问特定图形API功能的情况,自定义函数节点都能胜任。

自定义函数节点的核心价值在于它提供了不同级别的抽象控制。对于简单的自定义操作,可以直接在节点内部编写HLSL代码片段;对于更复杂的、可重用的功能,则可以引用外部的HLSL包含文件。这种灵活性使得它成为高级着色器开发中不可或缺的工具。

如何使用

创建自定义函数节点

在Shader Graph中创建自定义函数节点非常简单。通过Create Node Menu,您可以像添加其他标准节点一样添加自定义函数节点。创建后,节点默认不包含任何输入或输出端口,这需要您根据具体需求进行配置。

创建节点的步骤:

  • 在Shader Graph编辑器中右键点击空白区域
  • 从弹出的创建节点菜单中选择"Custom Function"
  • 新节点将出现在图中,初始状态下没有输入输出端口

配置节点属性

创建节点后,需要通过Graph Inspector进行详细配置。在Graph Inspector中展开Node Settings部分,您将看到两个主要的配置菜单:Custom Function菜单和Custom Port Menu菜单。

Custom Function菜单负责定义函数的行为和实现方式,而Custom Port Menu则用于定义节点的输入和输出接口。这两个菜单的配合使用使得自定义函数节点既强大又灵活。

Custom Function菜单详解

Custom Function菜单是配置节点核心功能的地方,包含多个重要的配置项:

输入输出端口定义

  • Inputs - 使用Custom Port Menu定义节点的输入端口。您可以添加任意数量和类型的输入,从简单的浮点数到复杂的纹理采样。
  • Outputs - 同样使用Custom Port Menu定义输出端口。输出可以是单个值,也可以是多个值的组合。

函数类型选择

Type字段决定了自定义函数的实现方式,有两个选项:

  • File - 引用外部HLSL文件中的函数
  • String - 直接在节点中编写HLSL代码

选择哪种类型取决于您的具体需求。对于简单的、一次性的代码片段,String模式更加方便;对于复杂的、可重用的函数,File模式更为合适。

函数命名规范

Name字段定义了在生成代码中使用的函数名称。这里有几个重要的注意事项:

  • 名称应该具有描述性,能够清晰表达函数的功能
  • 不要包含精度后缀(如_float_half),系统会自动添加
  • 遵循C#的函数命名规范,使用帕斯卡命名法

源文件引用

Source字段仅在File模式下可用,用于指定包含自定义函数的HLSL文件。这个文件应该放在项目的Assets文件夹内,通常建议放在专门的"Shaders"或"HLSL"文件夹中以便管理。

函数体编写

Body字段仅在String模式下可用,用于直接输入HLSL代码。这里编写的代码将被直接插入到生成的着色器中。

通过字符串定义函数

String模式适合那些相对简单、不需要复杂预处理或包含其他文件的HLSL代码。这种模式的优点是快速、直接,所有代码都集中在节点内部,便于管理和调试。

基本语法结构

在String模式下编写函数时,系统会自动处理函数的参数列表、大括号和缩进。您只需要关注函数的核心逻辑。例如,一个简单的颜色调整函数:

ini 复制代码
HLSL

// 简单的颜色亮度调整
Out = A * B;

系统会自动将其包装成完整的函数定义。

精度类型处理

在String模式下,可以使用$precision标记来代替具体的精度类型。这个标记会在编译时根据节点的精度设置自动替换为halffloat,确保代码的平台兼容性。

上图中的示例将生成以下完整的HLSL函数:

css 复制代码
HLSL

void MyFunction_float(float3 A, float B, out float3 Out)
{
    Out = A + B + 1/2;
}

实际应用示例

假设我们需要实现一个自定义的颜色混合效果,结合HSV色彩空间的调整:

scss 复制代码
HLSL

// 将RGB颜色转换为HSV
float3 rgbToHsv(float3 c)
{
    float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g));
    float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r));
    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

// 将HSV颜色转换回RGB
float3 hsvToRgb(float3 c)
{
    float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * lerp(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

// 主要的颜色调整函数
float3 adjustedColor = rgbToHsv(BaseColor);
adjustedColor.x += HueShift;        // 调整色相
adjustedColor.y *= Saturation;      // 调整饱和度
adjustedColor.z *= Brightness;      // 调整亮度
Out = hsvToRgb(adjustedColor);

通过文件定义函数

File模式提供了更大的灵活性和代码组织能力,特别适合复杂的着色器功能。通过引用外部HLSL文件,您可以:

  • 创建可重用的函数库
  • 包含其他HLSL文件
  • 定义uniform变量和常量
  • 实现跨多个着色器共享的功能

文件结构要求

当使用File模式时,HLSL文件需要遵循特定的结构。最基本的要求是包含适当的防护宏,防止重复包含:

arduino 复制代码
HLSL

//UNITY_SHADER_NO_UPGRADE
#ifndef MYHLSLINCLUDE_INCLUDED
#define MYHLSLINCLUDE_INCLUDED

// 您的函数实现在这里

#endif //MYHLSLINCLUDE_INCLUDED

UNITY_SHADER_NO_UPGRADE指令告诉Unity不要自动升级这个文件,保持代码的稳定性。

函数命名与精度处理

在File模式下,函数名称需要手动添加精度后缀。Unity会根据节点的精度设置自动调用相应后缀的函数:

css 复制代码
HLSL

void MyFunction_float(float3 A, float B, out float3 Out)
{
    Out = A + B;
}

void MyFunction_half(half3 A, half B, out half3 Out)
{
    Out = A + B;
}

Uniform变量定义

File模式的一个强大特性是可以在函数范围外定义uniform变量,这使得您可以访问在Shader Graph中定义的材质属性:

arduino 复制代码
HLSL

//UNITY_SHADER_NO_UPGRADE
#ifndef MYHLSLINCLUDE_INCLUDED
#define MYHLSLINCLUDE_INCLUDED

float4x4 _MyMatrix;
float _MyFloatParameter;

void MyFunction_float(float3 A, float B, out float3 Out)
{
    // 使用矩阵变换输入向量
    A = mul(float4(A, 0.0), _MyMatrix).rgb;
    // 使用浮点参数
    Out = A + B * _MyFloatParameter;
}

#endif //MYHLSLINCLUDE_INCLUDED

复杂函数组织

在同一个HLSL文件中,您可以定义多个相关的函数,并在主要函数中调用它们:

arduino 复制代码
HLSL

//UNITY_SHADER_NO_UPGRADE
#ifndef MYHLSLINCLUDE_INCLUDED
#define MYHLSLINCLUDE_INCLUDED

// 辅助函数:计算菲涅尔效应
float3 FresnelEffect_float(float3 normal, float3 viewDir, float power)
{
    float fresnel = pow(1.0 - saturate(dot(normal, viewDir)), power);
    return fresnel;
}

// 辅助函数:应用噪声扰动
float3 ApplyNoise_float(float3 position, float noiseScale, float noiseIntensity)
{
    // 简单的噪声函数实现
    float noise = sin(position.x * noiseScale) * cos(position.y * noiseScale);
    return position + normal * noise * noiseIntensity;
}

// 主要处理函数
void MyComplexFunction_float(float3 Position, float3 Normal, float3 ViewDir,
                            out float3 FinalColor, out float Alpha)
{
    // 计算菲涅尔效应
    float3 fresnel = FresnelEffect_float(Normal, ViewDir, 3.0);

    // 应用噪声扰动
    float3 distortedPos = ApplyNoise_float(Position, 10.0, 0.1);

    // 组合最终结果
    FinalColor = lerp(float3(0.1, 0.3, 0.8), float3(1.0, 0.9, 0.2), fresnel);
    Alpha = fresnel.r;
}

#endif //MYHLSLINCLUDE_INCLUDED

文件包含与模块化

File模式支持包含其他HLSL文件,这使得代码组织更加模块化:

arduino 复制代码
HLSL

//UNITY_SHADER_NO_UPGRADE
#ifndef MYHLSLINCLUDE_INCLUDED
#define MYHLSLINCLUDE_INCLUDED

#include "Assets/Shaders/NoiseFunctions.hlsl"
#include "Assets/Shaders/LightingFunctions.hlsl"

void MyAdvancedFunction_float(float3 worldPos, float3 normal, out float3 result)
{
    // 使用来自其他文件的函数
    float noise = SimplexNoise_float(worldPos * 5.0);
    float3 lighting = CalculateRimLight_float(normal, WorldSpaceViewDir);

    result = noise * lighting;
}

#endif //MYHLSLINCLUDE_INCLUDED

重用自定义函数节点

自定义函数节点本身是单实例的,但通过一些技术可以实现代码的重用,提高开发效率。

子图形转换

最直接的重用方法是将自定义函数节点转换为子图形。子图形是Shader Graph中的可重用组件,可以包含任意复杂的节点网络,包括自定义函数节点。

转换步骤:

  • 右键点击自定义函数节点
  • 选择"Convert to Sub Graph"
  • 保存子图形文件到项目中的合适位置

子图形中的自定义函数

在子图形中,您可以像在主图中一样使用自定义函数节点,但具有额外的优势:

  • 输入输出端口可以通过子图形的接口暴露
  • 可以在多个着色器之间共享
  • 支持参数化和配置

创建自定义函数库

通过组合多个自定义函数节点和子图形,可以创建功能丰富的着色器函数库:

  • 数学函数库(噪声、曲线、插值等)
  • 颜色处理函数库(色彩空间转换、滤镜效果等)
  • 几何处理函数库(UV变换、顶点动画等)
  • 光照计算函数库(BRDF、阴影计算等)

使用纹理线

从Unity 2019.3(URP 7.3)开始,Shader Graph引入了新的纹理数据结构,以确保自定义函数节点和子图形在处理纹理时的一致性。

新的纹理结构

Shader Graph现在使用五个专门的数据结构来处理纹理和采样器:

  • UnityTexture2D - 用于2D纹理
  • UnityTexture2DArray - 用于2D纹理数组
  • UnityTexture3D - 用于3D纹理
  • UnityTextureCube - 用于立方体贴图
  • UnitySamplerState - 用于采样器状态

升级旧的自定义函数

如果您有在早期版本中创建的自定义函数节点,需要更新它们以使用新的结构:

识别需要升级的节点

  • 在10.3或更高版本中,过时的节点会显示警告标志
  • 检查所有使用纹理输入或输出的自定义函数节点

升级步骤

. 修改输入输出类型

  • 将所有纹理相关的端口类型从"Bare"改为相应的结构类型
  • 例如:Texture2D → UnityTexture2D . 更新HLSL代码
  • 对于String模式,确保使用Unity的纹理采样宏
  • 对于File模式,用新结构替换旧的类型声明

新的访问方式

使用新结构后,访问纹理相关数据的方式发生了变化:

ini 复制代码
HLSL

// 旧方式(已过时)
float2 texelSize = _MyTexture_TexelSize;
SamplerState sampler = sampler_MyTexture;

// 新方式(推荐)
float2 texelSize = myInputTex.texelSize;
UnitySamplerState sampler = myInputTex.samplerstate;

实际升级示例

假设我们有一个旧的自定义函数节点,用于应用简单的纹理扭曲:

升级前的代码(过时)

scss 复制代码
HLSL

void DistortTexture_float(Texture2D MainTex, SamplerState Sampler,
                         float2 UV, float DistortionStrength,
                         out float4 OutColor)
{
    // 计算扭曲后的UV
    float2 distortedUV = UV + sin(UV.y * 10.0) * DistortionStrength;

    // 采样纹理
    OutColor = SAMPLE_TEXTURE2D(MainTex, Sampler, distortedUV);
}

升级后的代码(推荐)

scss 复制代码
HLSL

void DistortTexture_float(UnityTexture2D MainTex, float2 UV,
                         float DistortionStrength, out float4 OutColor)
{
    // 计算扭曲后的UV
    float2 distortedUV = UV + sin(UV.y * 10.0) * DistortionStrength;

    // 采样纹理(使用结构中的采样器状态)
    OutColor = SAMPLE_TEXTURE2D(MainTex.tex, MainTex.samplerstate, distortedUV);
}

平台兼容性考虑

新的纹理结构特别改善了在GLES2等平台上的兼容性。通过统一的接口,确保在不同图形API上的一致行为:

  • 自动处理不同平台的纹理采样差异
  • 简化多平台着色器开发
  • 提供一致的texelSize和samplerstate访问

最佳实践与性能考虑

使用自定义函数节点时,遵循一些最佳实践可以确保代码的性能和可维护性。

代码组织建议

  • 将相关的函数分组到同一个HLSL文件中
  • 使用有意义的函数和变量命名
  • 为复杂函数添加注释说明
  • 创建函数库而不是分散的单个函数

性能优化技巧

  • 在String模式中使用$precision标记确保精度一致性
  • 避免在片段着色器中执行复杂的循环或分支
  • 尽可能使用内置的HLSL函数而不是自定义实现
  • 合理使用精度限定符(half vs float)

调试与错误处理

  • 使用简单的测试用例验证自定义函数
  • 在HLSL代码中添加适当的边界检查
  • 利用Unity Frame Debugger检查生成的着色器代码
  • 注意编译错误和警告信息

版本控制与协作

  • 将HLSL文件纳入版本控制系统
  • 为复杂的自定义函数编写文档
  • 在团队项目中建立统一的编码规范
  • 使用有意义的文件和组织结构

【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

相关推荐
爱勇宝7 小时前
《完蛋!我被男同学包围了》为什么会火?不只是因为“高中生玩票”
游戏·游戏开发
_zhourui_h_1 天前
Unity MyFramework 用法说明(二十九):使用 CameraManager 统一管理游戏摄像机
unity3d
SmalBox1 天前
【节点】[Or节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_2 天前
Unity MyFramework 用法说明(二十七):使用 InputSystem 与 KeyMappingSystem 统一管理输入和快捷键
unity3d
_ZHOURUI_H_2 天前
Unity MyFramework 用法说明(二十五):使用 AtlasManager 统一管理图集与 Sprite 引用
ui·unity·游戏引擎·unity3d·游戏开发
_zhourui_h_2 天前
Unity MyFramework 用法说明(二十六):使用 RedPointSystem 构建树形红点逻辑
unity3d
SmalBox2 天前
【节点】[Not节点]原理解析与实际应用
unity3d·游戏开发·图形学
程序员码歌3 天前
我全程用 AI开发了一款微信小游戏,上线了
android·前端·游戏开发
万亿少女的梦1683 天前
基于Unity3D的双状态场景解密游戏设计与实现
unity3d·游戏开发·场景渲染·交互系统·叙事设计