Skip to content
HY Devlog
Go back

The Lite Version Doesn't Show Up in the Shader List

I clipped this while applying a toon shader. Earlier I cross-checked lilToon’s parameters entry by entry, and handling them took me all the way down to what a shader even is. This time the page came up while looking into the application procedure itself in detail — lilToon’s official 「はじめに」(Introduction), the entrance that packs installation through the shader variation list into one page.

The parameters post looked at Inspector entries; this one looks at what you choose before you ever reach that Inspector. Which install route, which shader, which Unity version it runs on. Being stuck on one unknown parameter costs less than choosing wrong at this entrance and finding out much later.

One thing snags. On the last row of the shader variation table, the docs recommend this.

Lite版から直接マテリアルを設定せず、通常版で作成したものを変換するとより直感的にマテリアル設定が可能です。

(Rather than setting up a material directly from the Lite version, converting one created with the normal version makes material setup more intuitive.)

The reason given is “more intuitive.” It reads like a matter of taste. Yet the shader name on that same row of that same table is Hidden/lilToonLite. A shader whose name begins with that never comes up in the shader dropdown of the Material Inspector at all. Conversion is recommended not because it’s more intuitive, but because it’s effectively the only route.

Table of contents

Table of contents

The Three Install Routes Don’t Give the Same Result

STEP 1 lists three install methods and says to take “any one of them” (どれか一つ).

RouteWhere it comes fromUpdating
.unitypackageDownload the zip from BOOTH → drag into the Project windowManual. Fetch the new version and import again
VPM (VCC / ALCOM)Register the vpm.json repository → install from the package listPick from the versions in the package list
UPM Git URLEnter the Git URL in Package ManagerPackage Manager’s Update

These do not come out the same. Where the three diverge is after installation.

A .unitypackage only unpacks files into the project. No version information is left in the project, so updating becomes “fetch a new zip and import it again.” Which means it overwrites files that are already there.

The docs themselves warn about that problem in another section — the one on distributing works made with lilToon.

シェーダー本体と制作物を1つのunitypackageにまとめる方法は、ユーザーがインポート時に古いバージョンで上書きしてしまう問題が発生する可能性があるため非推奨です

(Bundling the shader itself and your work into a single unitypackage is not recommended, because users may overwrite with an older version when they import.)

The route STEP 1 said to pick freely becomes the reason for a “not recommended” in the distribution section. The two sections aren’t contradictory. When I install it myself I know which version I have; once it’s baked into a distribution I don’t know the recipient’s. But “any one of them” hides that difference.

Unity’s docs attach their own conditions to the UPM Git URL route.

Install the Git client (minimum version 2.14.0) on your computer.

On Windows you also have to add the Git executable path to the PATH environment variable. And two cautions come with it.

there’s no guarantee about the package quality, stability, validity, or even whether the version stated in its package.json file respects Semantic Versioning rules.

If the Git repository uses Git LFS, the imported package might contain pointer files instead of the actual content.

Unlike the registry route, version rules aren’t guaranteed. That a repository using Git LFS may deliver pointer files instead of actual content is another trap the registry doesn’t have. In the post on installing Newtonsoft Json I looked at why you leave Package Manager’s version field blank; the Git URL route is the one where that version field doesn’t exist at all.

For a VRChat avatar the choice is effectively settled. It’s the one the docs recommend in the distribution section — tell users to install via VCC — and the VPM route is the only one that records the shader’s version in the project manifest.

A Shader’s Name Decides the Material Dropdown

The last table in the docs lists six shader variations. Written out exactly:

NamePurpose (per the docs)
lilToonThe main shader. Use this for general purposes
_lil/[Optional] lilToonOverlayA transparent shader for layering over a material. Unneeded passes are stripped, so it’s cheaper than layering a normal transparent shader
_lil/[Optional] lilToonOutlineOnlyOutline only. On a hard-edged model, assigning this to a separate mesh with smooth normals draws a cleaner outline
_lil/[Optional] lilToonFurOnlyDraws only the hair part of fur. Recommended for layering over a normal shader, or combining cutout fur with transparent fur
_lil/lilToonMultiThe version that uses shader keywords. Build size grows easily when a large number of materials is used
Hidden/lilToonLiteGreatly lightened while keeping some of the normal version’s look

These names aren’t descriptions, they’re addresses. Unity’s docs say so explicitly. The Shader.Find page describes the name it takes like this:

the name you can see in the shader popup of any material, for example ‘Standard’, ‘Unlit/Texture’, ‘Legacy Shaders/Diffuse’ etc.

So the string you write in Shader "..." is the path shown in a material’s shader popup. Slashes become submenus.

Starting with Hidden/ Means It’s Not in the List

The docs don’t explain what the Hidden/ prefix does. Neither does Unity’s manual. The ShaderLab page covering shader name syntax stops at the level of “Defines a Shader object with a given name.”

The behavior is in the editor source. ShaderDropdownDataBuilder.EnumerateShaders in MaterialEditor.cs, which builds the list for the Material Inspector’s shader dropdown, does this as its first filter.

var shaders = ShaderUtil.GetAllShaderInfo();
foreach (var shader in shaders)
{
    var shaderName = shader.name;
    if (shaderName.StartsWith("Deprecated") || shaderName.StartsWith("Hidden"))
        continue;
    // ...
}

It’s continue. Not folded into a submenu — dropped from the enumeration itself. So Hidden/lilToonLite isn’t in the dropdown no matter how hard you dig.

There’s a detail. The string being compared is "Hidden", not "Hidden/". No slash. A shader named HiddenThing disappears along with it. And no StringComparison is given, so it’s a culture-dependent comparison — the same spot as the Turkish I problem, though Hidden has no capital I, so it won’t bite in practice.

Line that up and the recommendation reads differently. “Don’t set up a material directly from the Lite version” is less a recommendation than a statement of fact. You can’t pick it from the dropdown. Instead lilToon puts a conversion feature on its optimization page — buttons that convert to the Lite version, the Multi version, or MToon (for VRM). That button is the route around Hidden/.

_lil/ Is a Submenu, Not a Prefix

Four entries in that table start with _lil/. The docs put it this way:

“_lil”内にあるシェーダーは特殊なものなので、基本的には通常の”lilToon”を選択してください

(The shaders inside _lil are special ones, so in general please select the normal lilToon.)

“Inside _lil” is the precise phrasing. The prefix carries no special meaning; the part before the slash becomes a menu folder name. _lil as a name is just a convention of prefixing an underscore so it sorts ahead of the alphabet.

The same EnumerateShaders splits the list by presence of a slash on the next lines.

if (shaderName.StartsWith("Legacy Shaders/")) { legacy.Add(shaderName); continue; }

if (!shaderName.Contains("/")) { unnested.Add(shaderName); continue; }

normal.Add(shader.name);

Names without a slash are collected separately into unnested. The two lists are then each sorted and emitted in normal → unnested order. So lilToon isn’t inside a submenu — it’s a top-level item — while the four _lil/... entries go into the _lil submenu.

The docs’ “in general select the normal lilToon” therefore means “don’t go into the submenu; pick the one at the top level.”

Being in the List Doesn’t Mean You Can Use It

The remaining branches of EnumerateShaders are worth reading too. Compile failure and lack of support are classified separately.

if (shader.hasErrors) { failed.Add(shaderName); continue; }
if (!shader.supported) { notSupported.Add(shaderName); continue; }

notSupported and failed aren’t removed from the list — they’re shown under their own classification. That connects to lilToon’s shader model table.

VariationShader model
NormalSM4.0 · ES3.0
LiteSM3.0 · ES2.0
FurSM4.0 · ES3.1+AEP · ES3.2
TessellationSM5.0 · ES3.1+AEP · ES3.2

Tessellation requires SM5.0. On a build target that doesn’t meet that, it does appear in the dropdown, marked unsupported. Seeing a name in the list and that name working are different stories.

What Point in Time the Support List Points At

The 「対応状況」(support status) section comes in four groups. The render pipeline list among them reads:

  • ビルトインレンダーパイプライン
  • ライトウェイトレンダーパイプライン
  • ユニバーサルレンダーパイプライン
  • ハイデフィニションレンダーパイプライン

The second line is the Lightweight Render Pipeline, i.e. LWRP. The third is URP. Both are listed. Yet Unity’s official migration guide opens with a single sentence.

The Universal Render Pipeline (URP) replaces the Lightweight Render Pipeline (LWRP) in Unity 2019.3.

URP replaced LWRP in 2019.3. As the post on render pipelines showed, a pipeline is specified as an asset, and the LWRP asset doesn’t exist from that version on. And the Unity version list on the same page reads 2022.3, 2023.1~2023.3. Because the lowest version claimed as supported is 2022.3, there isn’t a single project in that range that could be using LWRP. Two lists on the same page negate each other.

This isn’t plain neglect. lilToon’s developer documentation (shader structure) shows how pipeline support is implemented.

パイプライン対応(Built-in/LWRP/URP/HDRP)もスクリプトでlil_pipeline.hlslを書き換えることで実装されています

(Pipeline support (Built-in/LWRP/URP/HDRP) is also implemented by rewriting lil_pipeline.hlsl from a script.)

LWRP survives on the internal macro side. That line in the support list looks like the shader source’s branch names carried straight over. It’s a spot where a name left in code leaked out into the documentation’s support list.

The version list has a snag of its own: 2023.3. Unity never shipped that version number.

Unity 6 represents the beginning of the next generation of the Unity Engine and is the new official version name for what was previously referred to as Unity 2023 LTS.

The stream that was going to be 2023 LTS came out under the name Unity 6. 2023.1 and 2023.2 did ship as tech stream releases, but 2023.3 doesn’t exist as a final release number. The last line of the support list is pointing at a beta number.

So what was actually verified? The same section’s “verified environment” answers.

Unity 2022.3.22f1(ビルトインRP / URP 14.0.8 / HDRP 14.0.8)

Where that number came from is immediately clear from VRChat’s docs.

The current Unity version used by VRChat is 2022.3.22f1

It is safe to remain on VRChat’s supported version of the Unity editor (2022.3.22f1). Upgrading your version will result in content not loading once uploaded to VRChat

2022.3.22f1 is exactly the version VRChat requires. Identical down to the patch. lilToon’s support list is pinned to VRChat’s editor version, not to Unity’s current releases. For an avatar shader that’s a sensible choice, and for someone reading the docs it means the answer to “does it work on Unity 6?” is not in that list.

What Multi Pays for Its Keywords

The row in the variation table with the shortest description and the largest implication is _lil/lilToonMulti.

シェーダーキーワードを利用するバージョンです。マテリアルを大量に使用する場合などにビルドサイズが大きくなりやすいため注意が必要です。

(This is the version that uses shader keywords. Care is needed because build size grows easily when a large number of materials is used.)

Why keywords connect material count to build size is exactly the rule seen in the custom shaders post. Variants grow as the product of the keyword sets, and Unity’s docs state the consequence like this:

A large number of variants can result in increased build times, file sizes, runtime memory usage, and loading times.

What’s newly interesting here is the other side: the normal version doesn’t use keywords at all. That sentence from the developer docs above gives the mechanism — pipeline branching is produced by rewriting the HLSL file from a script. Feature toggling works the same way. lilToon’s shader settings page explains that the setting is common to all materials and that turning a feature off there removes it from the shader.

Put the two side by side and the trade shows.

NormallilToonMulti
Feature togglingShader settings (global, removed from source)Per-material shader keywords
Different feature combos per materialNo — the setting is globalYes
Variants as materials growDoesn’t growGrows per combination
Build sizeAs much as the features turned onAs much as the keyword combos used

The normal version gave up flexibility to fix its variant count; Multi did the opposite. The docs’ “care is needed when a large number of materials is used” points at the Multi side of that trade.

One thing here needs correcting. As the reason Multi reuses Unity’s standard keywords (_NORMALMAP, _EMISSION, _METALLICGLOSSMAP and so on), the developer docs cite avoiding keyword exhaustion. There was a period when that was a real constraint. Unity’s 2020.3 manual put it this way:

There is a limit of 384 global shader keywords, and Unity uses around 60 of them internally (therefore lowering the available number). Each individual shader has a limit of 64 local keywords.

But the same page in the 2022.3 manual — the version lilToon says it supports — carries different numbers.

Unity can use up to 4,294,967,294 global shader keywords. Individual shaders and compute shaders can use up to 65,534 local shader keywords.

From 384 to 4.29 billion. Within the supported version range, the global keyword count is no longer a scarce resource. The remaining constraint is a different line.

If a shader uses more than 128 keywords in total, it incurs a small runtime performance penalty; therefore, it is best to keep the number of keywords low.

So what you’re weighing when choosing Multi today is not exhaustion but the 128 line and the variant count. And having many materials isn’t itself a cost — the post on what a shader is had the SRP Batcher docs nail that down. What splits batches isn’t material count but shader variants. That’s precisely why Multi’s cost scales with material count. It isn’t expensive because materials grew; it’s expensive because the keyword combination differs per material.

Writing the Terminology Table Again

The introduction page opens with a 「登場する用語」(terms you’ll encounter) table — eight rows for someone handling 3DCG for the first time. That table is also where Korean secondary material breaks down most often. The translation I received read like this:

In the clippingJapanese originalWhat it should be
재료 — “raw material”マテリアルMaterial (머티리얼)
질감 — “texture” as a tactile qualityテクスチャTexture (텍스처)
일반 지도 — “ordinary map”ノーマルマップNormal map (노멀 맵)
매트 캡 — split into two wordsマットキャップMatCap (매트캡)
모피 / 털털 — “fur coat”ファーFur (퍼)
북 셰이더 — “book shader”本シェーダー“this shader” (본 셰이더)

マテリアル became raw material by way of the other sense of the English word material. ノーマルマップ was decomposed into normal (ordinary) + map and came out as ordinary map. The hardest to recognize is the last row. The license section’s 本シェーダー means “this shader,” but 本 was read as book, producing a book shader.

The parameters post covered 「the rim light’s monetary fine」 — 細さ (thinness) → fine → a monetary fine, two hops. The same kind of accident happens in the terminology table.

This translation has an accident of a different class too. A sentence like this sits in the middle of STEP 2:

The Committee recommends that the State party take all necessary measures to ensure that all children are provided with adequate and appropriate health services, including adequate food, clothing, shelter, medical and health care.

This is shader documentation. STEP 2 in the original has no corresponding sentence — something close to a child-rights committee recommendation was generated where nothing stood. That’s a different layer of problem from mistranslation. A mistranslation carries the original across wrongly; this is something that was never in the original arriving anyway. Wrong terminology can be recovered by cross-checking, but with this you have to see the original to know it exists at all.

Written out with correct terms, the table becomes this:

TermDescription (per the original)
MaterialData that decides how something looks
TextureAn image. Used for many things, such as deciding something’s color
UVData that decides where a texture is applied
MaskA texture used to specify which part gets processed
Normal mapA texture that makes a surface look as if it has bumps
MatCapA texture with light reflection drawn into it
Rim lightA light where light wraps around as if backlit, brightening only the outline
StencilA mask expression performed on screen

The relation between material and texture draws the same line as the Unity definition in the post on what a shader is — a material is “an asset that defines how a surface should be rendered” and holds a shader reference inside it. The introduction page’s “data that decides how something looks” is that definition compressed into one line.

The texture assignment table is worth rewriting too. A row like the translation’s 「ordinary map setting → ordinary map → ordinary map」 can’t be found in the Inspector.

Texture typeInspector path
Main textureBase Color Setting → Main Color → Texture
Normal mapNormal Map Setting → Normal Map → Normal Map
Outline maskOutline Setting → Outline → Mask & Width
Shadow maskShadow Setting → Shadow → Mask & Strength
MatCapMatCap Setting → MatCap → MatCap
MatCap maskMatCap Setting → MatCap → Mask
Rim light maskRim Light Setting → Rim Light → Color / Mask
Emission (mask)Emission Setting → Emission Texture → Color

Where and Why You’d Use It

Checking Which Variation a Material Uses

Once an avatar has more than ten materials, checking which is the normal version and which is a converted Lite one by hand in the Inspector gets tedious. The shader name is the classification, so you can sweep it in code.

using System;
using UnityEngine;

public class ShaderVariantReport : MonoBehaviour
{
    private const string HIDDEN_PREFIX = "Hidden/";
    private const string SUBMENU_PREFIX = "_lil/";
    private const string LILTOON_NAME = "lilToon";

    [Header("Targets")]
    [SerializeField, Tooltip("Renderers to inspect. Leave empty to gather all children")]
    private Renderer[] _targets;

    private void Start()
    {
        if (_targets == null || _targets.Length == 0)
        {
            // Include inactive objects.
            _targets = GetComponentsInChildren<Renderer>(true);
        }

        foreach (Renderer target in _targets)
        {
            // Read sharedMaterials. Reading materials clones every one of them.
            foreach (Material material in target.sharedMaterials)
            {
                if (material == null)
                {
                    continue;
                }

                string shaderName = material.shader.name;
                Debug.Log($"{target.name} / {material.name} → {shaderName} [{Classify(shaderName)}]", target);
            }
        }
    }

    private static string Classify(string shaderName)
    {
        // State StringComparison explicitly. Unity's editor source omits it.
        if (shaderName.StartsWith(HIDDEN_PREFIX, StringComparison.Ordinal))
        {
            return "not in the dropdown";
        }

        if (shaderName.StartsWith(SUBMENU_PREFIX, StringComparison.Ordinal))
        {
            return "_lil submenu";
        }

        if (shaderName == LILTOON_NAME)
        {
            return "normal version";
        }

        return "not lilToon";
    }
}

Why the material == null comparison isn’t ?. is in the post on Fake Null. A Unity object keeps its managed reference after being destroyed, and ?. doesn’t filter that state out.

The sharedMaterials choice is deliberate too. Reading materials clones every one of the renderer’s materials. Code that only reads ending up multiplying materials is the trap seen with Renderer.material in the earlier post; here reading is all that’s needed, so it looks at the shared ones.

Switching to Lite by Quality Level

Since the Lite version isn’t in the dropdown, a runtime switch takes the shape of swapping in a material converted ahead of time in the editor. Not swapping the shader out — the normal and Lite versions have different property sets.

using UnityEngine;

[RequireComponent(typeof(Renderer))]
public class QualityMaterialSwap : MonoBehaviour
{
    private const int DEFAULT_LITE_THRESHOLD = 2;

    [Header("Materials")]
    [SerializeField, Tooltip("Material authored with the normal version")]
    private Material _normalMaterial;

    [SerializeField, Tooltip("Material converted to Lite in the editor")]
    private Material _liteMaterial;

    [Header("Switch")]
    [SerializeField, Range(0, 5), Tooltip("Use Lite at or below this quality level")]
    private int _liteThreshold = DEFAULT_LITE_THRESHOLD;

    private void Awake()
    {
        if (!TryGetComponent(out Renderer targetRenderer))
        {
            return;
        }

        bool useLite = QualitySettings.GetQualityLevel() <= _liteThreshold;
        Material selected = useLite ? _liteMaterial : _normalMaterial;

        if (selected == null)
        {
            return;
        }

        // Cloning happens when you 'read' material. Here the asset is assigned as is.
        targetRenderer.sharedMaterial = selected;
    }
}

Holding both materials as Inspector references is the point. There’s a route that finds them by name, but that one trips in the build. From the Shader.Find docs:

A shader might be not included into the player build if nothing references it. In that case, Shader.Find will work only in the Editor, and will result in the pink error shader in a build.

It works in the editor and turns pink in the build. The first of the three remedies the docs offer is “reference it from materials used in your scene,” and the [SerializeField] material references above create exactly that condition. The other two are adding it to Always Included Shaders under ProjectSettings/Graphics, and putting the shader or material in a Resources folder.

Where Not to Use It

Wrapping Up


References

The starting point for this post was a Korean-language scrape of the はじめに page of lilToon’s official docs. I re-checked the terminology and install steps against the Japanese original, and confirmed how shader names are handled in the dropdown from Unity’s editor source.


Share this post:

Previous Post
The Definition Describes a Pixel Shader, Not a Shader
Next Post
Create an asmdef and Assembly-CSharp Goes Dark