AnyPortrait > Script > Custom Shader
AnyPortrait provides a variety of commonly used shaders, but depending on the project, you may need to write a custom shader with special effects.
You can easily write a custom shader to create great rendering results by following just a few rules of AnyPortrait.
This page explains how AnyPortrait's shader basics are structured.
Based on this explanation, we hope you will write and apply a custom shader.
When you want to apply shaders you wrote, you can use the Material Library or change the properties of the mesh.
Please check the following manuals for how to change shaders.
- Material Library
- Shader for meshes
Note
This page does not cover how to write shaders.
For more information, please refer to the Unity documentation, forums, and related sites!
AnyPortrait does not use a single shader, but rather uses a set of shaders that are appropriate for the situation.
A total of 17 shaders are applied as a set based on three factors from which shaders are derived. (Related page)
The factors are as follows.
1. Color Space
The shader is determined based on the color space of the project.
It is written based on Gamma Space, and the color calculation formula changes in the case of Linear Space.
2. Blending
Meshes are rendered using one of four blending methods.
The default is Alpha Blend, with Additive, Soft Additive, and Multiplicative slightly modifying the blending properties and the code related to alpha calculations.
3. Clipping
The mesh being clipped receives mask texture and performs additional alpha calculations.
There is also one shader that generates the mask, but it is a functional shader that performs a fixed role.
The above factors are combined to apply a total of 17 shaders to the character.
However, when you want to write a custom shader, you don't necessarily have to write all of them.
Just check the following and write only the shaders that fit the situation of your project.
- Write only one type of shader, either Gamma or Linear, depending on the color space of the project.
- If you do not use the blending option, you only need to write the Alpha Blend shader as default.
- If there is no clipped mesh, you do not need to write a clipping shader.
- The Alpha Mask shader, which creates a clipping mask, only performs a fixed role, so you do not need to create it as a custom shader.
This page first explains the rules and method for writing an "Alpha Blend" shader in the default "Gamma Space".
Then it explains the parts that need to be modified depending on the factors that determine the shader.
It might also be helpful to check out the shader codes built into the AnyPortrait package.
The shader assets are located by default in "Assets/AnyPortrait/Assets/Shaders".
It might be convenient to duplicate the provided shaders and modify the necessary parts.
If you want to create a custom shader with Shader Graph, you can write it in the format of a Shader Graph by following the rules described on this page.
For more information, see the Related page
AnyPortrait's default shader is not much different from Unity's default Transparent shader.
The following code is the default shader for "Alpha Blend" in "Gamma Space", and you can add your own custom code based on it.
Note
AnyPortrait supports both Surface Shader, Fragment Shader, and Shader Graph.
This page explains based on the code of Fragment Shader, and you can apply the rules explained here appropriately according to the situation.
Shader "Sample Shader/Gamma Space - Normal - AlphaBlend"
{
Properties
{
_Color ("2X Color (RGBA Mul)", Color) = (0.5, 0.5, 0.5, 1.0)
_MainTex ("Main Texture (RGBA)", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "PreviewType" = "Plane" }
Blend SrcAlpha OneMinusSrcAlpha
LOD 200
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Color.rgb * 2.0f;
col.a *= _Color.a;
return col;
}
ENDCG
}
Pass
{
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
V2F_SHADOW_CASTER;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
float4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.a *= _Color.a;
if(col.a < 0.05f)
{
discard;
}
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
Let's take a look at the main parts of the shader code.
1. Properties
_Color ("2X Color (RGBA Mul)", Color) = (0.5, 0.5, 0.5, 1.0)
_MainTex ("Main Texture (RGBA)", 2D) = "white" {}
These are the properties used in the default Transparent shader of Unity and the properties used in AnyPortrait.
In general cases where the mesh is not clipped, only these properties are used.
Therefore, in custom shaders, all codes related to these two properties must be written as is.
Also, since the AnyPortrait system controls these properties, you should not modify these properties with other scripts.
2. Basic Pass
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off
...
: Here is the code for the basic rendering pass.
The basic pass is specified as above because there is a separate pass for shadow casting.
If you do not write a shadow pass, you can omit the "Tags { "LightMode" = "ForwardBase" }" part.
Since we do not do Z Write, we set this value to Off.
3. Blending
Blend SrcAlpha OneMinusSrcAlpha
: Blending code for the "Alpha Blend" shader.
4. Color calculation
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Color.rgb * 2.0f;
col.a *= _Color.a;
return col;
: AnyPortrait's color operation formula is "2X Multiply".
So RGB needs to be multiplied by 2 additionally.
This code changes depending on blending.
5. Shadow Pass
Pass
{
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
...
}
: For the "Alpha Blend" mesh, you need to be able to generate shadows depending on the option.
Therefore, you need to write a "ShadowCaster" pass that generates shadows.
This pass only plays a role in generating shadows, so you can write it as in the example above.
If you write it in the Surface Shader way, you don't need to write this pass.
The above example is shader code in Gamma Space, if the color space of the project is Linear Space, you need to modify the "Color calculation" part.
In the above basic code, let's modify the frag function of the basic pass ("ForwardBase") as follows.
...
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Color.rgb * 4.595f;
col.rgb = pow(col.rgb, 2.2f);
col.a *= _Color.a;
return col;
}
...
In Linear Space, if a value raised to the power of 2.2 is applied to the color compared to Gamma Space, it appears closer to the original image color.
So, even though it is the "2X Multiply" operation rule as above, we multiply it by "4.595" instead of "2", and the final RGB is also raised to the power of "2.2".
This change should be applied regardless of the blending type.
By changing the mesh options, you can render in the "Alpha Blend", "Additive", "Soft Additive", and "Multiplicative" methods in addition to the default blending method.
You need to write a shader for each blending method, but you can easily write it by slightly modifying the code of "Alpha Blend" above.
Blending options other than "Alpha Blend" are mainly for visual effects, so it is recommended not to write a pass that generates shadows.
For the three types of blending, you just need to modify some codes in the shader, "Blend" and "frag function".
Also, unlike Alpha Blend, we omit the shadow generation pass, so you can delete the "LightMode" designation code.
The following example includes the codes for all three types of blending, so you can check the comments and compile the necessary codes.
...
SubShader
{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "PreviewType" = "Plane" }
// --- Select and write according to the blending type ---
Blend One One // Additive
Blend OneMinusDstColor One // Soft Additive
Blend DstColor SrcColor // Multiplicative
LOD 200
Pass
{
// --- Delete Pass Tags ---
// Tags { "LightMode" = "ForwardBase" }
ZWrite Off
CGPROGRAM
...
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Color.rgb * 2.0f;
col.a *= _Color.a;
// --- Additive 또는 Soft Additive ---
col.rgb *= col.a;
col.a = 1.0f;
// ---------------------------------
// --- Multiplicative ---
col.rgb = col.rgb * (col.a) + float4(0.5f, 0.5f, 0.5f, 1.0f) * (1.0f - col.a);
col.a = 1.0f;
// ----------------------
return col;
}
ENDCG
}
// Delete Shadow Generation Pass
}
}
1. Blending Options
Blend One One // Additive
Blend OneMinusDstColor One // Soft Additive
Blend DstColor SrcColor // Multiplicative
: Depending on your mesh's blending options, you can choose one of the above codes.
(Soft Additive may not be supported if you use Shader Graph.)
2. Delete Multi Pass
// Tags { "LightMode" = "ForwardBase" }
: Since the shadow generation pass is gone, you can delete the code related to the multi-pass.
Also delete the shadow generation pass code.
3. Modify Color calculation
(1) Additive or Soft Additive
col.rgb *= col.a;
col.a = 1.0f;
(2) Multiplicative
col.rgb = col.rgb * (col.a) + float4(0.5f, 0.5f, 0.5f, 1.0f) * (1.0f - col.a);
col.a = 1.0f;
: Change the final color to match the blending, and change the Alpha value to 1.
The color calculation formulas for Additive and Soft Additive are the same.
The Clipped meshes are rendered by a separate shader.
The shader for clipping receives the rendering result of the mesh that creates the clipping mask in texture format and adds an operation to prevent some of it from being rendered.
You can add the clipping property and the clipping operation to the basic shader.
Also, the clipping shader does not need to generate shadows in Alpha Blend, so you can write it as a single pass.
This is because it is included in the shadow of the mask mesh.
The following example is a shader for Alpha Blend in Gamma Space, and can be modified to match the explanation above.
Shader "Sample Shader/Gamma Space - Clipped - AlphaBlend"
{
Properties
{
_Color ("2X Color (RGBA Mul)", Color) = (0.5, 0.5, 0.5, 1.0)
_MainTex ("Main Texture (RGBA)", 2D) = "white" {}
// --- Clipping properties ---
_MaskTex ("Mask Texture (A)", 2D) = "white" {}
_MaskScreenSpaceOffset ("Mask Screen Space Offset (XY_Scale)", Vector) = (0, 0, 0, 1)
}
SubShader
{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "PreviewType" = "Plane" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
LOD 200
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
// --- Screen Position for Clipping Operations ---
float4 screenPos : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
// --- Property variables for Clipping ---
sampler2D _MaskTex;
float4 _MaskScreenSpaceOffset;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// --- Calculating Screen Position for Clipping ---
o.screenPos = ComputeScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Color.rgb * 2.0f;
col.a *= _Color.a;
// --- Set Alpha channel by Clipping mask ---
float2 screenUV = i.screenPos.xy / max(i.screenPos.w, 0.0001f);
screenUV -= float2(0.5f, 0.5f);
screenUV.x *= _MaskScreenSpaceOffset.z;
screenUV.y *= _MaskScreenSpaceOffset.w;
screenUV.x += _MaskScreenSpaceOffset.x * _MaskScreenSpaceOffset.z;
screenUV.y += _MaskScreenSpaceOffset.y * _MaskScreenSpaceOffset.w;
screenUV += float2(0.5f, 0.5f);
col.a *= tex2D(_MaskTex, screenUV).r;
return col;
}
ENDCG
}
}
}
1. Properties
_MaskTex ("Mask Texture (A)", 2D) = "white" {}
_MaskScreenSpaceOffset ("Mask Screen Space Offset (XY_Scale)", Vector) = (0, 0, 0, 1)
: Add a mask texture to the property for clipping mask processing.
If you add a property like the above, AnyPortrait will automatically handle clipping rendering.
_MaskScreenSpaceOffset is a property to apply the clipping mask based on the screen coordinate system.
sampler2D _MaskTex;
float4 _MaskScreenSpaceOffset;
: You also need to add variables to match the added properties.
2. Screen Space Position Semantic
float4 screenPos : TEXCOORD1;
: Clipping masks are processed in screen space.
Therefore, the position of the vertex in screen space must be calculated in the vertex shader and passed to the fragment shader.
o.screenPos = ComputeScreenPos(o.vertex);
: Add code to calculate the position in screen coordinates in the vertex shader.
3. Setting alpha channel value by mask
float2 screenUV = i.screenPos.xy / max(i.screenPos.w, 0.0001f);
screenUV -= float2(0.5f, 0.5f);
screenUV.x *= _MaskScreenSpaceOffset.z;
screenUV.y *= _MaskScreenSpaceOffset.w;
screenUV.x += _MaskScreenSpaceOffset.x * _MaskScreenSpaceOffset.z;
screenUV.y += _MaskScreenSpaceOffset.y * _MaskScreenSpaceOffset.w;
screenUV += float2(0.5f, 0.5f);
col.a *= tex2D(_MaskTex, screenUV).r;
: This is the formula for calculating the clipping mask and applying it to the alpha channel.
Please write it as it is, as it is a unique formula used in the AnyPortrait system.
You can use keywords to compile a single shader code into multiple versions.
You can use keywords in shaders using the features added in AnyPortrait v1.5.1.
You can see related information about keywords in the following official Unity documentation.
- Unity Manual
Here's an example shader where color operations change based on the keyword "SPECIAL_COLOR".
Shader "Shader Example/Gamma Space - Normal - AlphaBlend (Keyword)"
{
Properties
{
_Color ("2X Color (RGBA Mul)", Color) = (0.5, 0.5, 0.5, 1.0)
_MainTex ("Main Texture (RGBA)", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "PreviewType" = "Plane" }
Blend SrcAlpha OneMinusSrcAlpha
LOD 200
Pass
{
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile __ SPECIAL_COLOR
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Color.rgb * 2.0f;
col.a *= _Color.a;
#ifdef SPECIAL_COLOR
col.rgb *= fixed3(0.0f, 1.0f, 0.0f);
#endif
return col;
}
ENDCG
}
}
}
1. Declaring keywords
#pragma multi_compile __ SPECIAL_COLOR
: Declare keywords using the #pragma multi_compile syntax.
In this example, a new keyword called "SPECIAL_COLOR" is declared.
You can use keywords in Unity using the "multi_compile" and "shader_feature" directives that generate shader variants, or the "dynamic_branch" directive that generates dynamic branches.
However, since AnyPortrait generates materials dynamically, you cannot use "shader_feature" that compiles shader variants based on assets.
2. Writing code branches
#ifdef SPECIAL_COLOR
col.rgb *= fixed3(0.0f, 1.0f, 0.0f);
#endif
: Write to behave differently depending on the keyword.
If you use "multi_compile", use the "#ifdef, #else, #elif, #endif" directives, and if you use "dynamic_branch", use the "if ( )" statement.
In this example, if the "SPECIAL_COLOR" keyword is enabled, only the color of the green channel is rendered.
To use keywords, you need to set them in Material Library.
For more information, please refer to the Related page.
(1) Open the Material Library.
(2) Add a property option named "SPECIAL_COLOR", set it to "Keyword" type, and set it to "Enable".
If you bake and run it, you can see that the rendering result changes depending on whether the keyword is activated or not, as shown above.