Shader Generator
ORL Shader Definition
As mentioned in the credits section of the main page - the ORL Shader package is built with the ORL Shader Generator which works by assembling a bunch of separated Modules into a single shadder based on a Template and a core Lighting Model. At the root of it all is an ORL Shader Definition, and its corresponding format - .orlshader
.
Shader Anatomy
The .orlshader
file supports multiple blocks all of which are optional, except for the %ShaderName()
.
You can see all of the blocks available to you out of the box in the built-in templates in the shader example at the bottom of this page.
Provided blocks
Beyond the configuration blocks like %ShaderName()
and %LightingModel()
- everything else is template-dependent. So a %ShaderDefines
block will only be embedded in the shader if the base template of the lighting model has a %ShaderDefines
hook inside of it.
This means, if you're using a custom template, you can add any new directives you want, and they will be inserted into the template as-is. You can read more about custom templates over here
%ShaderName(string name)
Required. Defines the name on which the shader will be registered in unity.
%ShaderName("orels1/Standard")
%LightingModel(string path)
Defines the lighting model to use, defaults to PBR
if not specified.
%LightingModel("@/LightingModels/Toon") // will pick the Toon lighting model from the shader generator package
Lighting models are special kinds of .orlsource
modules,that specify where the rest of the code gets included in the shader using the target
keyword in their %Includes()
block. Check out the built in LightingModels to see how they work.
You can also create your own lighting models, and use them in your shaders. Check out the Creating Lighting Models guide for more info.
Path Resolution
ORL Shader definitions automatically resolve paths to built-in assets if they start with @/
.
For your own dependencies you can either provide relative paths, or absolute paths that start with /Assets
or /Packages
.
%Template(string path)
Defines the template to use. Is meant to be defined inside of the LightingModel, but can be overriden here if needed.
%Template("@/Templates/Toon")
%CustomEditor(string className)
Defines the custom editor to use, no default is provided, but you can use the ORL Shader Inspector by specifying the following
%CustomEditor("ORL.ShaderInspector.InspectorGUI")
%TemplateFeatures(string[] features)
Defines the template features to use. Template features a special blocks of the source Template that are only included when the specified feature is listed in this block
%TemplateFeatures("MyFeature", "PrePass")
%Includes()
Contains the list of other shader modules to include, can be .orlshader
shader files or .orlsource
modules. The order of the includes is important, as the modules will be included in the order they are specified. The resolver will recursively follow the trail of includes and pull in any submodules if they are needed.
Self Include
You should use self
to include current shader's module in a specific spot
%Includes()
{
"@/Modules/AudioLink", // include a module from the generator package
"@/Shaders/ORL Standard", // include a base shader form the shaders package
"../MyModule", // include a module from the parent folder,
"Assets/OtherModule", // include a module from the project folder,
"self" // mount point for the current shader
}
Batteries Included
To make the development process nice and easy, a lot of things come pre-included, so unless defined otherwise - the following defaults will be provided:
- A PBR
LightingModel
- A set of
FragmentData
,VertexData
,MeshData
andSurfaceData
structs - A base
VertexFunction
- A pack of Utility functions like
remap
,invLerp
,HSV2RGB
, etc (check outUtilities.orlsource
in the generator package) - A CoreRP sampling library for unified cross-platform cross-pipleine sampling macros
%CheckedInclude(string path)
Includes a shader include file (e.g. a .cginc
or .hlsl
) if that file actually exists on disk. This is useful when working around conitional includes via keywords that sometimes produce errors in console.
%CheckedInclude("Assets/MyCustomStuff/MyInclude.cginc")
%Properties()
Contains your shader properties, uses regular ShaderLab properties syntax
%Properties()
{
UI_FaderHeader ("# Block Fader", Int) = 0
_Progress ("Progress", Range(0, 1.1)) = 0
_FaderColumns ("Fader Columns", Int) = 10
[ToggleUI]_FaderRemap ("Enable Fader Remapping", Int) = 0
_FaderRemapMin ("Min", Float) = 0.15
_FaderRemapMax ("Max", Float) = 0.95
}
%ShaderFeatures()
Contains a list of shader feature / multi compile pragmas for the shader
%ShaderFeatures()
{
#pragma shader_feature_local DETAILS_OVERLAY
}
%ShaderDefines()
Contains a list of defines for the shader
Built in LightingModels provide some addition features you can request by specifying special defines, see below
%ShaderDefines()
{
#if !defined(PLAT_QUEST)
#define _INTEGRATE_CUSTOMGI
#endif
}
%ShaderTags()
Contains a list of top-level tags that will be appended to the Tags { }
list
%ShaderTags()
{
"Queue" = "AlphaTest" "RenderType" = "TransparentCutout"
}
%PassTags()
Same as ShaderTags but for individual passes
%PassTags()
{
"LTCGI"="Always"
}
%PassModifiers()
Contains a list of extra modifiers that will be appended right below tags in the ForwardBase
pass
Usually used for things like Blend
keywords or Cull Off
, etc.
%PassModifiers()
{
Blend SrcAlpha OneMinusSrcAlpha
}
Some built-in LightingModels (like Toon and PBR) also support modifying other passes, like:
%AddPassModifiers()
{
// Modifiers for ForwardAdd
}
%MetaPassModifiers()
{
// Modifiers for Meta
}
%ShadowPassModifiers()
{
// Modifiers for ShadowCaster
}
%PrePassModifiers()
{
// Modifiers for the PrePass
// Only available in the Toon template
}
%PassModifiers
are resolved in the order of inclusion. For example, if you have a %PassModifiers()
block in your .orlshader
file directly - its values will override the values in the FragmentBase
files of your lighting model.
%ShaderModifiers()
Same as %PassModifiers()
but for the entire shader. You can override the Shader Modifiers by specifying individual Pass Modifiers
%ShaderModifiers()
{
ZWrite Off
}
%ShaderModifiers
are resolved in the order of inclusion. For example, if you have a %ShaderModifiers()
block in your .orlshader
file directly - its values will override the values in the FragmentBase
files of your lighting model.
%Variables
Contains a list of variables used in the pass. You must declare all the shared variables (like the ones bound to the properties) here so they can be de-duplicated across all included modules
E.g., half _Cutoff
or half4 _MainTex_ST
etc
%Variables()
{
half _Progress;
int _FaderColumns;
int _FaderRemap;
half _FaderRemapMin;
half _FaderRemapMax;
}
%Fragment(string functionName)
Contains your fragment code that will be injected into the Fragment stage of the shader
You should define it as a void
function that takes in w/e shader data you might need to be included, you can see the list of built-in Data structs here
%Fragment("CutoutFragment")
{
void CutoutFragment(SurfaceData o)
{
#if !defined(_NATIVE_A2C)
if (o.Alpha < _Cutoff)
{
clip(-1);
}
#endif
}
}
Function Names
You must provide a function name, and it must be unique across all included modules, otherwise it will get deduplicated. Providing a function name allows you to include other functions in the same code block, which can be useful for refactoring.
%Vertex(string functionName)
Same as Fragment but now its injected into the vertex stage
%PostVertex(string functionName)
Same as Vertex but this one is injected after the main VertexBase
function of the Lighting Model. This is useful if you want to modify the vertex data after the main vertex function has already run.
%Color(string functionName)
Contains your FinalColor modifier code. It is appended after the lighting calculations have already been done, and allows to affect the very final output of the shader
Modifying anything but the FinalColor
variable here will not have any effect
%Color("MyColor")
{
void MyColor(inout half4 FinalColor)
{
FinalColor.rgb *= (sin(_Time.y) + 1) / 2.0;
}
}
%PrePassColor(string functioName)
Same as Fragment but this one is injected into the PrePass stage of the Toon template.
PrePass stage is only available when the PrePass
template feature is enabled.
Read more about template features here.
%Shadow(string functionName)
Contains your custom shadowcaster function. By Default it does not have access to any mesh data or fragment shader evaluation results for performance reasons.
You can change that behavior and run the full set of fragment functions by using a special define see below
Order Overload
All of the function directives can optionally take an order
int parameter, like this
%Framgent("MyFragment", -100)
{
void MyFragment(SurfaceData o)
{
// some code
}
}
This will make this fragment function be called before functions with no order specified, and after functions with a higher order specified.
So if your function gets included like this
%Includes()
{
"self",
"MyAwesomeModuleWithFunction"
}
Then despite the module being included below the self
keyword, its Fragment function will still be called before the self
fragment function.
This is primarily useful for maintaining a particular order of properties, while adjusting how the actual code gets executed. For example, the ORL Standard shader uses this to call Parallax UV adjustments before any other function while injecting its properties below the MainSettings of the base shader it is included from.
%PrePasses()
Contains a list of extra passes that will be appended at the beginning of the shader before the main passes.
Note that these do not use the Variables
and Textures
in Built-In render pipeline, so you'll have manually define the full contents of the pass.
All of the LibraryFunctions, however, including the sampling library and utilities, will be available to you.
%PrePasses()
{
GrabPass { _GrabTexture }
Pass
{
Cull Off
CGPROGRAM
// some shader code
ENDCG
}
}
%PostPasses()
Contains a list of extra passes that will be appended at the end of the shader after the main passes.
Note that these do not use the Variables
and Textures
in Built-In render pipeline, so you'll have manually define the full contents of the pass.
All of the LibraryFunctions, however, including the sampling library and utilities, will be available to you.
%PostPasses()
{
Pass
{
Cull Off
CGPROGRAM
// some shader code
ENDCG
}
}
%ExtraPass(string passName)
Contains blocks for an extra pass. This allows you to add extra generated passes to the shader, which will leverage all of the features of existing passes.
Template Requiremenets
This requires an ExtraPass
template to be available for the current template. E.g. if the current template is Toon
, then the ToonExtraPass.orltemplate
file must be present somewhere Shader Generator can find it. Which, at the moment, means it must be in the same Templates
folder.
You can nest most of the block types inside the %ExtraPass
. It will also inherit all the blocks of the current shader apart from any function blocks, e.g. %Vertex
, %Fragment
, etc. Those are expected to be defined in the %ExtraPass
block to implement your desired effect.
Current ExtraPass templates are using ForwardBase
light mode.
%ExtraPass("Wave")
{
%PassModifiers()
{
Blend One One
}
%Vertex("WaveVertex")
{
void WaveVertex(inout VertexData v)
{
float mask = sin(_Time.y * .8 - v.vertex.x * 0.1) * 0.5 + 0.5;
v.vertex.y += (sin(_Time.y * 1.2 + length(v.uv0.xy) * 10)) * 0.5 * mask + 0.8 * mask;
}
}
%Fragment("WaveFragment")
{
void WaveFragment(MeshData d, inout SurfaceData o)
{
float mask = sin(_Time.y * 0.8 - d.localSpacePosition.x * 0.1) * 0.5 + 0.5;
o.Albedo = _Color;
o.Albedo *= mask;
}
}
}
Optional Features
The built-in templates allow you to enable optional features by specifying some special defines in your %ShaderDefines
section
NEED_FRAGMENT_IN_SHADOW
: Forces the shadowcaster pass to execute all of the included fragment functions (except the lighting calculation), useful if you want to utilize the final calculated alpha to augment the shadow silhouette.NEED_FRAGMENT_IN_PREPASS
: When using Toon template withPrePass
TemplateFeature
enabled - forces the prepass to execute all of the included fragment functions. Majority of the time, to save performance, you probably want to reimplement the bare minimum of the calculations inside a customPrePassColor
function instead of using this define.EXTRA_V2F_0
,EXTRA_V2F_1
,EXTRA_V2F_2
,EXTRA_V2F_3
: Tells the templates to compile in extra float4s in the Vertex stage so you can pass some custom data to your Fragment stage, see the struct definition belowNEED_UV4
,NEED_UV5
,NEED_UV6
,NEED_UV7
: Tells the templates to include UV channels 4-7 in the Vertex stage and pass them to the Fragment stage._INTEGRATE_CUSTOMGI
: LEGACY Enables support for custom GI injection on top of built-in GI. Only avaible in the PBR Lighting Model.- You must define a function of the following signature inside of the
%Fragment()
blockIntegrateCustomGI(MeshData d, SurfaceData o, inout half3 indirectSpecular, inout half3 indirectDiffuse)
. This function will be called if the_INTEGRATE_CUSTOMGI
is defined. - Check out
Packages/sh.orels.shaders.generator/Runtime/Sources/Modules/LTCGI.orlsource
for reference.
- You must define a function of the following signature inside of the
_INTEGRATE_CUSTOMGI_FLEX
Enables support for custom GI injection on top of the built-in GI in the PBR Lighting Model. Compared to_INTEGRATE_CUSTOMGI
, this version allows you to define a function with an arbitrary call signature, which is useful for more complex GI implementations.
%CustomGI("MyGIFunction")
{
void MyGIFunction(MeshData d, inout half3 indirectSpecular)
{
indirectSpecular += pow(saturate(dot(d.worldNormal, d.worldSpaceViewDirection)), 10);
}
}
Since the call sign is arbitrary - you can access any variable available in the PBR FragmentBase function's scope at the %CustomGIFunctions
hook point.
%CustomGI("MyGIFunction")
{
void MyGIFunction(MeshData d, half3 envBRDF, inout half3 indirectSpecular, inout half3 indirectDiffuse)
{
indirectDiffuse += envBRDF;
}
}
Mesh and Surface Data
ORL Shader Generator's built-in templates provide all the relevant data via a couple of structs, which are described below
All of the structs are located in /Packages/sh.orels.shaders.generator/Runtime/Sources/Structs
. With root-level folder containing structs shared between the templates, and individual folders containing LightingModel specific structs
VertexData
You will be manipulating and accessing this in the vertex functions
The vertex data should be accessed via the v
variable, for example
void MyVertex(VertexData v)
{
v.vertex.xyz += v.normal.xyz * 0.1;
}
struct VertexData
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 color : COLOR;
float4 uv0 : TEXCOORD0;
float4 uv1 : TEXCOORD1;
float4 uv2 : TEXCOORD2;
float4 uv3 : TEXCOORD3;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
FragmentData
Unless you're planning to roll your own lighting models, the most you should do is use the extraV2F slots via the optional feature defines.
// first - declare that you need an extra v2f in your defines
%ShaderDefines()
{
#define EXTRA_V2F_0
}
// then pass data in your vertex function
%Vertex("MyVertex")
{
void MyVertex(inout FragmentData o)
{
o.extraV2F0.r = mySuperRandomFunction();
}
}
// then consume data in your fragment function
%Fragment("MyFragment")
{
void MyFragment(FragmentData i)
{
o.Emission.rgb = i.extraV2F0.r;
}
}
- First, you define that you need an extra v2f variable, you can request up to 3, see Optional Features
- Then you pass it via the
FragData
variable - Then you consume it via the
MeshData
variable -d
, e.g.,d.extraV2F0.xyz
MeshData
Contains all of the relevant vectors and parameters you might want to use in your Fragment/FinalColor code. Passed as a variable called d
. Can vary per lighting model, but here is the one from the PBR/VFX one at the time of writing. Check out /Packages/sh.orels.shaders.generator/Runtime/Sources/Structs/PBR
for the latest version.
struct MeshData
{
half2 uv0;
half2 uv1;
half2 uv2;
half2 uv3;
#if !defined(UNITY_PASS_SHADOWCASTER)
half4 lightmapUv;
#endif
half4 vertexColor;
half3 normal;
half3 worldNormal;
half3 localSpacePosition;
half3 worldSpacePosition;
half3 worldSpaceViewDir;
half3 tangentSpaceViewDir;
float3x3 TBNMatrix;
float4 extraV2F0;
float4 extraV2F1;
float4 extraV2F2;
float4 extraV2F3;
float4 screenPos;
};
For example - you can access mesh UVs like this
%Fragment("MyFragment")
{
void MyFragment(MeshData d)
{
half2 uv = d.uv0.xy;
}
}
SurfaceData
This is the struct that will be used in the lighting function. The PBR template consumes all of the provided values, while the VFX template only uses Albedo
, Emission
, and Alpha
to construct the FinalColor. And the Toon shader has its own set of values. Check out /Packages/sh.orels.shaders.generator/Runtime/Sources/Structs
to see whats available.
Here's the PBR SurfaceData struct at the time of writing
struct SurfaceData
{
half3 Albedo;
half3 Emission;
half Metallic;
half Smoothness;
half Occlusion;
half3 Normal;
half Alpha;
};
For example, this is how you would make your material look red and metallic. Because red goes fasta'
%Fragment("MyFragment")
{
void MyFragment(inout SurfaceData o)
{
o.Albedo = half3(1, 0, 0);
o.Metallic = 1;
}
}
Don't forget to add inout
to any struct which values you plan to modify! Otherwise they won't propagate to the next stage
FinalColor
FinalColor
is a value that contains the result of the final lighting calculation. You can modify its value using the Color Function, see Color function above
For example, this is how you would lower the overall brightness of the material no matter what kind of calculations happened prior to that
%Color("MyColor")
{
void MyColor(inout half4 FinalColor)
{
FinalColor.rgb *= 0.5; // halves the overall values
}
}
VFX Template
In the VFX template FinalColor
is constructed from o.Albedo
and o.Alpha
, so the Fragment
function essentially operates on the FinalColor directly. As such, there is no need to use a ColorFunction
in there.
Facing attribute
A float facing
parameter is also passed into the function. This allows you to detect if the current fragment is rendering the front face or the backface of the triangle.
Shader Example
Below is an example of a shader with all the blocks supported by the built-in toon template. You can also find this example in the /Packages/sh.orels.shaders.generator/Runtime/Resources/ORL Shader Example.txt
%ShaderName("My Awesome Shader")
%LightingModel("@/LightingModels/Toon")
%CustomEditor("ORL.ShaderInspector.InspectorGUI")
%Properties()
{
UI_MainHeader("# My Awesome Settings", Int) = 0
_Level("Level", Range(0,1)) = 0
_Mask("Mask", 2D) = "black" {}
_Brightness("Brightness", Range(0,2)) = 1
_Cutoff("Cutoff", Range(0, 1)) = 0.5
}
%Includes()
{
"@/Shaders/ORL Standard",
"self"
}
%ShaderFeatures()
{
#pragma shader_feature_local FANCY_FEATURE
}
%ShaderDefines()
{
#define MY_THING
}
%ShaderTags()
{
"Queue" = "AlphaTest" "RenderType"="TransparentCutout"
}
%PassTags()
{
"MyTag"="MyValue"
}
%PassModifiers()
{
Cull Off
}
%AddPassModifiers()
{
Blend One One
}
%MetaPassModifiers()
{
Cull Off
}
%ShadowPassModifiers()
{
Cull Off
}
%PrePasses()
{
GrabPass { _GrabTexture }
}
%Variables()
{
float _Level;
float4 _Mask_ST;
float _Brightness;
}
%Textures()
{
TEXTURE2D(_Mask);
SAMPLER(sampler_Mask);
}
%Fragment("MyFragment")
{
void MyFragment(MeshData d, inout SurfaceData o)
{
half2 uv = d.uv0.xy * _Mask_ST.xy + _Mask_ST.zw;
half mask = SAMPLE_TEXTURE2D(_Mask, sampler_Mask, uv).r;
o.Albedo = mask * _Level;
}
}
%Vertex("MyVertex")
{
void MyVertex(inout VertexData v)
{
v.vertex.xyz += v.normal * _Level * 0.1;
}
}
%Color("MyColor")
{
void MyColor(inout half4 FInalColor)
{
FinalColor.rgb *= _Brightness;
}
}
%Shadow("MyShadow")
{
void MyShadow(MeshData d)
{
half2 uv = d.uv0.xy * _Mask_ST.xy + _Mask_ST.zw;
half mainAlpha = SAMPLE_TEXTURE2D(_Mask, sampler_Mask, uv).a;
if (mainAlpha < _Cutoff)
{
clip(-1);
}
}
}
%PostPasses()
{
Pass
{
CGPROGRAM
// shader code
ENDCG
}
}