自定义函数节点是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标记来代替具体的精度类型。这个标记会在编译时根据节点的精度设置自动替换为half或float,确保代码的平台兼容性。
上图中的示例将生成以下完整的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 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)