name: liltoon-source-analysis category: research description: Systematic approach for analyzing lilToon shader source code — tracing the alpha/dissolve pipeline execution order, finding property definitions, and understanding how shader features interact. Based on lil_common_frag.hlsl + lil_pass_forward_normal.hlsl + lil_common_functions.hlsl analysis. trigger: When you need to understand how lilToon processes alpha, dissolve, main2ndTexture, main3rdTexture, alphamask, or any other fragment-shader feature; when debugging why a dissolve or alpha animation isn't working as expected
lilToon Source Code Analysis Guide
hy a dissolve or alpha animation isn't working as expected
lilToon Source Code Analysis Guide## Core Files (in order of relevance)
| File | Purpose |
|---|---|
lil_pass_forward_normal.hlsl |
Main fragment shader — defines the execution order of all OVERRIDE_* macros |
lil_common_frag.hlsl |
Fragment helper functions — lilGetMain2nd(), lilGetMain3rd(), OVERRIDE_DISSOLVE, OVERRIDE_ALPHAMASK, OVERRIDE_DITHER |
lil_common_frag_alpha.hlsl |
SubPass 透明系统 — 第二个 Fragment Pass 的 alpha 处理,仅 LIL_RENDER==2 时启用。包含独立的 AlphaMask + Dissolve + Cutoff 处理,使用 _SubpassCutoff(独立于主 Pass 的 _Cutoff) |
lil_common_functions.hlsl |
Low-level calculate functions — lilCalcDissolve(), lilCalcDissolveWithNoise() |
lil_common_input_opt.hlsl |
Shader property declarations — all _Main2nd*, _Main3rd*, dissolve params, _AlphaMaskMode |
lil_common_input_base.hlsl |
Base property declarations |
lil_common_macro.hlsl |
Macros, defines, LIL_RENDER values, 管线检测宏, LIL_SUBPASS_TRANSPARENT_MODE 定义(默认 0=Cutout, 1=Dither) |
lil_pipeline_hdrp.hlsl |
HDRP 管线定义 — 仅 30 行,定义 LIL_HDRP 宏和阴影模式 |
ltspass_transparent.shader |
Transparent 模式的 SubPass 实现 — 包含 FORWARD + FORWARD Alpha 双 Pass,#define LIL_RENDER 2 |
lts_trans.shader / lts_o.shader / lts_cutout.shader |
实际 shader 选择文件(通过 UsePass 引用 pass 实现) |
lil_common.hlsl |
顶层 include,自动包含对应管线的 pipeline 文件 |
Repository location: /root/lilToon/Assets/lilToon/Shader/Includes/ 和 ./Shader/ltspass_*.shader
Repository location**: /root/lilToon/Assets/lilToon/Shader/Includes/ 和 ./Shader/ltspass_*.shader### SubPass 分析要点
SubPass 的 Fragment Shader 在 lil_common_frag_alpha.hlsl 中,包含的步骤:
1. UDIM Discard
2. Main UV 动画
3. Main Color / Outline Color
4. 2nd/3rd Layer Color
5. AlphaMask(与主 Pass 相同逻辑:OVERRIDE_ALPHAMASK)
6. Dissolve(与主 Pass 相同逻辑:OVERRIDE_DISSOLVE)
7. Dither(与主 Pass 条件不同)
8. Cutout:clip(fd.col.a - _Cutoff) 和 clip(alphaRef - _SubpassCutoff)
关键配置:lil_common_macro.hlsl 第 16 行 #define LIL_SUBPASS_TRANSPARENT_MODE 0 — 模式 0=Cutout(硬切),模式 1=Dither(抖动)。需要修改时在包含前 #undef 重新定义。
Shader 选择与 LIL_RENDER 的关系
| Shader 文件 | Shader 名称 | LIL_RENDER | RenderType | Queue | SubPass |
|---|---|---|---|---|---|
lts_o.shader |
lilToon |
0 (Opaque) | Opaque | Geometry | ❌ |
lts_cutout.shader |
Hidden/lilToonCutout |
1 (Cutout) | TransparentCutout | AlphaTest | ❌ |
lts_trans.shader |
Hidden/lilToonTransparent |
2 (Transparent) | TransparentCutout | AlphaTest+10 | ✅(UsePass 引用 ltspass_transparent) |
lts_twotrans.shader |
Hidden/lilToonTwoPassTransparent |
2 | Transparent | Transparent | ✅ |
ltsmulti.shader |
lilToonMulti |
0 (默认) | Opaque | Geometry | ❌ |
_TransparentMode 是编辑器属性,不是编译期条件。它由 lilToon Inspector 脚本(C#)用于自动切换材质的 shader 文件。修改 _TransparentMode → 自动调用 mat.shader = Shader.Find("Hidden/lilToonCutout") → 改变 LIL_RENDER。
文件。修改 _TransparentMode → 自动调用 mat.shader = Shader.Find("Hidden/lilToonCutout") → 改变 LIL_RENDER。### HDRP 分析要点
lil_pipeline_hdrp.hlsl(/root/lilToon/Assets/lilToon/Shader/Includes/)内容:
- 定义 LIL_HDRP 宏
- 设置 LIGHTLOOP_DISABLE_TILE_AND_CLUSTER
- 默认 SHADOW_LOW
- Include 多个 HDRP 内置包
- 阴影通过 SHADOW_LOW/MEDIUM/HIGH 宏控制
- VRChat 使用 BRP — HDRP 适配主要是独立 Unity 项目场景
管线自动检测:lilToon 通过检查 Unity 定义的内置宏自动选择 BRP/URP/HDRP,不需要手动修改。
Step-by-Step Analysis Method
Step 1: Find the execution order
In lil_pass_forward_normal.hlsl, search for the frag() function. All processing happens through OVERRIDE_* macros in a fixed sequence:
grep -n 'OVERRIDE_' /root/lilToon/Assets/lilToon/Shader/Includes/lil_pass_forward_normal.hlsl | grep -v '^#\|defined\|#if'
This reveals the exact pipeline order. For the main (non-outline) path, you'll see something like:
OVERRIDE_MAIN → OVERRIDE_NORMAL_1ST → OVERRIDE_NORMAL_2ND → OVERRIDE_AUDIOLINK →
OVERRIDE_MAIN2ND → OVERRIDE_MAIN3RD → OVERRIDE_ALPHAMASK → OVERRIDE_DISSOLVE →
OVERRIDE_DITHER → [Alpha/Cutoff] → LIL_PREMULTIPLY → OVERRIDE_DISSOLVE_ADD
Key insight: Each OVERRIDE_ is a macro defined in lil_common_frag.hlsl that calls specific functions.
sight: Each OVERRIDE_ is a macro defined in lil_common_frag.hlsl that calls specific functions.### Step 2: Understand how OVERRIDE macros work
Most OVERRIDE macros are defined as function calls in lil_common_frag.hlsl:
// Pattern:
#if !defined(OVERRIDE_MAIN2ND)
#define OVERRIDE_MAIN2ND \
lilGetMain2nd(fd, color2nd, main2ndDissolveAlpha LIL_SAMP_IN(sampler_MainTex));
#endif
The actual function body is directly in the same file (e.g., void lilGetMain2nd(...)).
Step 3: Trace special wrappers around OVERRIDE_DISSOLVE
The dissolve macro in lil_pass_forward_normal.hlsl is NOT called directly — it's wrapped with special logic:
BEFORE_DISSOLVE
#if defined(LIL_FEATURE_DISSOLVE) && LIL_RENDER != 0
float dissolveAlpha = 0.0;
if (fd.dissolveActive) {
float priorAlpha = fd.col.a;
fd.col.a = 1.0f;
OVERRIDE_DISSOLVE // ← this is defined in lil_common_frag.hlsl
if (fd.dissolveInvert) fd.col.a = 1.0f - fd.col.a;
fd.col.a *= priorAlpha;
}
#endif
This means dissolve always:
1. Saves current fd.col.a (which may have been modified by AlphaMask or Main2nd/3rd)
2. Sets fd.col.a = 1.0 (dissolve works on fully opaque)
3. Calls the dissolve function which multiplies fd.col.a *= dissolveMaskVal
4. Multiplies result back into the original alpha
ion which multiplies fd.col.a *= dissolveMaskVal
4. Multiplies result back into the original alpha### Step 4: Trace the dissolve function itself
In lil_common_functions.hlsl, both lilCalcDissolve() and lilCalcDissolveWithNoise() follow the same pattern:
void lilCalcDissolve(inout float alpha, inout float dissolveAlpha, ...) {
dissolveParams.xy = round(dissolveParams.xy); // mode, shape
if(dissolveParams.r) { // r > 0 means dissolve is enabled
// ... calculate dissolveMaskVal based on shape
alpha *= dissolveMaskVal; // ← modifies alpha directly
}
}
Critical insight: The alpha parameter passed is whatever variable was fed in:
- Main dissolve → fd.col.a (the accumulated final alpha)
- Main2nd dissolve → color2nd.a (only the 2nd texture alpha)
- Main3rd dissolve → color3rd.a (only the 3rd texture alpha)
Step 5: Check guarding macros
Many features are conditionally compiled. Always check:
# See what guards exist around a feature
grep -B5 -A2 '_DissolveParams' lil_common_frag.hlsl | grep '#if\|#endif'
ards exist around a feature
grep -B5 -A2 '_DissolveParams' lil_common_frag.hlsl | grep '#if\|#endif'# Example: Main2nd dissolve requires LIL_FEATURE_LAYER_DISSOLVE
Common feature guards:
- LIL_FEATURE_DISSOLVE — main dissolve enabled
- LIL_FEATURE_LAYER_DISSOLVE — Main2nd/Main3rd dissolve enabled
- LIL_FEATURE_ALPHAMASK — AlphaMask enabled
- LIL_FEATURE_MAIN2ND / LIL_FEATURE_MAIN3RD — 2nd/3rd texture layers
- LIL_LITE — if defined, Main2nd/Main3rd/Shadow/etc are completely removed
- LIL_RENDER != 0 — non-Opaque modes only (dissolve/alphamask skip Opaque)
- LIL_RENDER == 0 — Opaque: fd.col.a = 1.0 (ignores ALL alpha processing)
Step 6: Property definition locations
Properties are split across lil_common_input_opt.hlsl and lil_common_input_base.hlsl:
# Find where a property is declared
grep -n '_DissolveParams\|_Main2ndTex' lil_common_input_opt.hlsl
Note that some properties (like _UseMain2ndTex) use the lilBool type, which is a float under the hood.
t some properties (like _UseMain2ndTex) use the lilBool type, which is a float under the hood.## Alpha Pipeline Reference (complete order)
For non-outline, non-Opaque (LIL_RENDER != 0) rendering:
| Order | Step | What happens to fd.col.a |
|---|---|---|
| 1 | OVERRIDE_MAIN |
Sets fd.col.a = _MainTex.a * _Color.a |
| 2 | OVERRIDE_MAIN2ND |
Can overwrite/multiply fd.col.a via _Main2ndTexAlphaMode (1-4). Also: if LIL_FEATURE_LAYER_DISSOLVE, modifies color2nd.a independently |
| 3 | OVERRIDE_MAIN3RD |
Same as step 2 but for 3rd texture |
| 4 | OVERRIDE_ALPHAMASK |
fd.col.a = saturate(alphaMask * scale + value) — 5 modes (0=off, 1=overwrite, 2=multiply, 3=add, 4=subtract) |
| 5 | OVERRIDE_DISSOLVE |
Saves prior alpha → fd.col.a=1.0 → modifies fd.col.a via dissolve → restores prior |
| 6 | OVERRIDE_DITHER |
(Cutout only) dither comparison |
| 7 | Alpha cutoff | Cutout: saturate((a - _Cutoff) / fwidth + 0.5); Transparent: clip(a - _Cutoff) |
| 8 | LIL_PREMULTIPLY |
fd.col.rgb *= fd.col.a (Transparent only) |
| 9 (SubPass only) | SubPass clip(alphaRef - _SubpassCutoff) |
仅 LIL_RENDER==2 模式,在 SubPass 中再次执行 AlphaMask + Dissolve 后用 _SubpassCutoff 裁切 |
| 9 (forward only) | OVERRIDE_DISSOLVE_ADD |
fd.emissionColor += _DissolveColor.rgb * dissolveAlpha |
| 10 (forward only) | Layer dissolve add | fd.emissionColor += _Main2ndDissolveColor.rgb * main2ndDissolveAlpha (if LIL_FEATURE_LAYER_DISSOLVE) |
Critical Insights
dDissolveColor.rgb * main2ndDissolveAlpha(ifLIL_FEATURE_LAYER_DISSOLVE`) |
Critical Insights### 2nd/3rd Dissolve vs Main Dissolve — Different alpha targets
Main dissolve passes fd.col.a directly → multiplies fd.col.a *= dissolveMaskVal.
2nd/3rd dissolve passes color2nd.a / color3rd.a → multiplies texture layer alpha, then this modified alpha is applied via _Main2ndTexAlphaMode/_Main3rdTexAlphaMode.
This means you can independently dissolve texture layers without affecting the base texture's dissolve. For example: main texture fully visible, 2nd texture dissolving in for a "pattern appears" effect.
Opaque mode kills all alpha
When LIL_RENDER == 0:
Main2nd/Main3rd require LIL_FEATURE_LAYER_DISSOLVE for their own dissolve
Without this compile-time macro, _Main2ndDissolveParams / _Main3rdDissolveParams are completely ignored — the dissolve code path is not compiled in.
Both outlines and main path process dissolve the same way
In lil_pass_forward_normal.hlsl, there are two code paths: #if defined(LIL_OUTLINE) and #else (main). Both apply the same OVERRIDE_ALPHAMASK → OVERRIDE_DISSOLVE → OVERRIDE_DITHER → Alpha sequence.
Bash Command Cheatsheet
# Full execution order
grep -n 'OVERRIDE_' lil_pass_forward_normal.hlsl | grep -v 'defined\\|#if\\|^$\\|ONLY\\|LIL_OUTLINE'
grep -n 'OVERRIDE_' lil_pass_forward_normal.hlsl | grep -v 'defined\\|#if\\|^$\\|ONLY\\|LIL_OUTLINE'# Find a property in all input files
grep -rn '_Main2ndTex\\b' lil_common_input*.hlsl
# Check which guard macros protect a section
sed -n '/lilGetMain2nd/,/^void lilGetMain3rd/p' lil_common_frag.hlsl | grep '#if\\|#else\\|#endif'
# See all dissolve-related code in functions file
grep -n 'Dissolve\\|dissolve' lil_common_functions.hlsl
# Check if a feature is available in Lite
grep -n 'LIL_LITE\\|MAIN2ND\\|MAIN3RD\\|LAYER_DISSOLVE' lil_common_frag.hlsl
# Count lines of each type of processing
grep -c 'OVERRIDE_' lil_pass_forward_normal.hlsl
# Find LIL_RENDER values for Transparent mode files
grep -n '#define LIL_RENDER' lts_trans.shader lts_twotrans.shader ltspass_transparent.shader
# Check SubPass configuration
grep -n 'LIL_SUBPASS_TRANSPARENT_MODE' lil_common_macro.hlsl
# Check pipeline detection and attachment light mode
grep -n '#elif defined(LIL_HDRP)\\|LIL_ADDITIONAL_LIGHT_MODE' lil_common_macro.hlsl
# Read the HDRP pipeline file (only 30 lines)
cat /root/lilToon/Assets/lilToon/Shader/Includes/lil_pipeline_hdrp.hlsl
- Found the frag() function in lil_pass_forward_normal.hlsl
- Identified OVERRIDE macro execution order
- Checked
LIL_RENDERguards — is the feature available in Opaque/Cutout/Transparent? - Checked
LIL_LITEguards — is the feature available in Lite mode? - Checked feature macros (LIL_FEATURE_*) for variant availability
- Checked what variable (fd.col.a vs color2nd.a vs color3rd.a) is passed to each dissolve call
- Verified property names in lil_common_input_opt.hlsl match the ones used in AnimationClip bindings