




Unityで黒にしてるのは
最低値を定義/設定するためです。


D:\Github\ENRAI_CLO3D_UnityGit\VRChat_Dev_Prj\Assets\Shaders\UnlitAlbedo.shader
// ライティング・影・トーンマップ等の影響を受けず、テクスチャの色をそのまま出力する Unlit シェーダー。
// Built-in RP / VRChat (Single Pass Instanced 対応)。
Shader "Custom/UnlitAlbedo"
{
Properties
{
_MainTex ("Albedo (RGBA)", 2D) = "white" {}
_Color ("Color (multiply)", Color) = (1, 1, 1, 1)
_Brightness ("Brightness (multiply)", Float) = 1
_AmbientInfluence ("Ambient Color Influence (0 = texture as is, 1 = x ambient light color)", Float) = 0
_AmbientCustom ("Ambient Custom (0 = world ambient color, 1 = RGB below)", Float) = 0
_AmbientR ("Ambient Custom R", Float) = 0
_AmbientG ("Ambient Custom G", Float) = 0
_AmbientB ("Ambient Custom B", Float) = 0
[Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 2
_Cutoff ("Alpha Cutoff (0 = off)", Range(0, 1)) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" "IgnoreProjector"="True" }
Pass
{
// ShadeSH9 (環境光/ライトプローブ) の値は ForwardBase パスでのみ確実に渡される
Tags { "LightMode"="ForwardBase" }
Cull [_Cull]
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float _Brightness;
float _AmbientInfluence;
float _AmbientCustom;
float _AmbientR;
float _AmbientG;
float _AmbientB;
half _Cutoff;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert(appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_OUTPUT(v2f, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv) * _Color;
if (_Cutoff > 0) clip(col.a - _Cutoff);
// 環境光の色 (ワールドのライティング設定 / ライトプローブ) を、_AmbientInfluence の割合だけ乗算で混ぜる
// 法線を使わず (0,0,0) で評価すると SH の定数項だけ = 向きに依存しない環境光の平均色 1 色になる
float3 worldAmbient = ShadeSH9(float4(0, 0, 0, 1));
// _AmbientCustom > 0 でワールドの環境光の代わりに、自分で選んだ RGB を使う (VRChat のスライダーで 3 値を動かす)
float3 ambient = lerp(worldAmbient, float3(_AmbientR, _AmbientG, _AmbientB), saturate(_AmbientCustom));
float3 rgb = col.rgb * lerp(float3(1, 1, 1), ambient, saturate(_AmbientInfluence));
return fixed4(rgb * _Brightness, 1);
}
ENDCG
}
}
Fallback Off
}
VRC Furyでの設定例
albedShaderBrightness

albedShaderAmbient

albedShaderAmbientCustom

albedShaderAmbientR

albedShaderAmbientG

albedShaderAmbientB

っていうのが面倒くさいのでツールにした

D:\Github\ENRAI_CLO3D_UnityGit\VRChat_Dev_Prj\Assets\Editor\BlendShapeToggleBuilder\AlbedBrightnessSliderBuilder.cs
using System;
using System.Collections.Generic;
using System.Linq;
using com.vrcfury.api;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
// 指定シェーダー (既定 Custom/UnlitAlbedo) を使うレンダラーを集め、そのシェーダーの Float プロパティ
// (_Brightness / _AmbientInfluence など) を 0%→100% で動かす VRCFury スライダー Toggle
// (Use a Slider (Radial)) を、一覧のチェックした行ぶんまとめて生成する。
// PublicApi に Material Property アクションが無いので、そこだけリフレクションで追加する。
public class AlbedBrightnessSliderBuilder : EditorWindow
{
const string ActionTypeName = "VF.Model.StateAction.MaterialPropertyAction";
[Serializable]
class Entry
{
public bool enabled = true;
public string property;
public string menuPath;
public float valueAt100 = 1f;
public float defaultPercent; // スライダーの初期位置 (0〜100%)。VRChat での起動時の値になる
}
// 一覧の初期内容を変えたら上げる。保存済みウィンドウの古い一覧を作り直すため
const int EntriesVersion = 2;
[SerializeField] GameObject _target;
[SerializeField] string _shaderName = "Custom/UnlitAlbedo";
[SerializeField] int _entriesVersion;
[SerializeField] List<Entry> _entries = new List<Entry>();
List<Renderer> _renderers = new List<Renderer>();
Vector2 _scroll;
[MenuItem("Tools/VRCFury/Albed Brightness Slider Builder")]
static void Open()
{
GetWindow<AlbedBrightnessSliderBuilder>("Albed Sliders");
}
void OnEnable()
{
if (_entries == null || _entries.Count == 0 || _entriesVersion != EntriesVersion) ResetEntries();
OnSelectionChange();
}
void ResetEntries()
{
_entries = DefaultEntries();
_entriesVersion = EntriesVersion;
}
static List<Entry> DefaultEntries()
{
return new List<Entry>
{
new Entry { property = "_Brightness", menuPath = "albedShaderBrightness", defaultPercent = 100 },
new Entry { property = "_AmbientInfluence", menuPath = "albedShaderAmbient" },
new Entry { property = "_AmbientCustom", menuPath = "albedShaderAmbientCustom" },
new Entry { property = "_AmbientR", menuPath = "albedShaderAmbientR", defaultPercent = 100 },
new Entry { property = "_AmbientG", menuPath = "albedShaderAmbientG", defaultPercent = 100 },
new Entry { property = "_AmbientB", menuPath = "albedShaderAmbientB", defaultPercent = 100 },
};
}
// シーン上のオブジェクトを選ぶと、そのアバター最上位 (root) が Target になる
void OnSelectionChange()
{
var go = Selection.activeGameObject;
if (go != null && !EditorUtility.IsPersistent(go) && go.transform.root.gameObject != _target)
{
_target = go.transform.root.gameObject;
Refresh();
}
Repaint();
}
void Refresh()
{
_renderers = _target == null
? new List<Renderer>()
: _target.GetComponentsInChildren<Renderer>(true)
.Where(r => r.sharedMaterials.Any(UsesShader))
.ToList();
}
bool UsesShader(Material m)
{
return m != null && m.shader != null && m.shader.name == _shaderName;
}
static Type FindActionType()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(ActionTypeName, false))
.FirstOrDefault(t => t != null);
}
void OnGUI()
{
EditorGUI.BeginChangeCheck();
_target = (GameObject)EditorGUILayout.ObjectField(
new GUIContent("Target Object", "Toggle を追加する GameObject (アバター最上位)。シーン上のオブジェクトを選ぶと自動で入る"),
_target, typeof(GameObject), true);
_shaderName = EditorGUILayout.TextField("Shader Name", _shaderName);
if (EditorGUI.EndChangeCheck()) Refresh();
var targetOk = _target != null && !EditorUtility.IsPersistent(_target);
if (!targetOk)
{
EditorGUILayout.HelpBox("Target Object にシーン上の GameObject を指定してください。", MessageType.Info);
return;
}
if (GUILayout.Button("Refresh")) Refresh();
EditorGUILayout.LabelField($"'{_shaderName}' を使うレンダラー {_renderers.Count} 個", EditorStyles.boldLabel);
foreach (var r in _renderers) EditorGUILayout.LabelField(" " + r.name);
EditorGUILayout.Space();
EditorGUILayout.LabelField("スライダー (チェックした行を追加)", EditorStyles.boldLabel);
using (var scroll = new EditorGUILayout.ScrollViewScope(_scroll))
{
_scroll = scroll.scrollPosition;
Entry remove = null;
foreach (var e in _entries)
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
using (new EditorGUILayout.HorizontalScope())
{
e.enabled = EditorGUILayout.ToggleLeft(e.property, e.enabled, EditorStyles.boldLabel);
if (GUILayout.Button("×", GUILayout.Width(22))) remove = e;
}
e.property = EditorGUILayout.TextField("Property", e.property);
e.menuPath = EditorGUILayout.TextField("Menu Path", e.menuPath);
e.valueAt100 = EditorGUILayout.FloatField(
new GUIContent("Value at 100%", "スライダー 100% のときのプロパティ値 (0% はマテリアルの現在値)"), e.valueAt100);
e.defaultPercent = EditorGUILayout.Slider(
new GUIContent("Default %", "スライダーの初期位置。VRChat で起動した直後のこの値になる"), e.defaultPercent, 0f, 100f);
DrawWarnings(e);
}
}
if (remove != null) _entries.Remove(remove);
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("+ 行を追加")) _entries.Add(new Entry { property = "_Property", menuPath = "albedShader" });
if (GUILayout.Button("初期値に戻す")) ResetEntries();
}
var count = _entries.Count(e => e.enabled);
using (new EditorGUI.DisabledScope(_renderers.Count == 0 || count == 0))
{
if (GUILayout.Button($"Add Slider Toggles ({count})", GUILayout.Height(28))) Build();
}
}
void DrawWarnings(Entry e)
{
if (string.IsNullOrEmpty(e.property)) return;
var mats = _renderers.SelectMany(r => r.sharedMaterials).Where(UsesShader).Distinct().ToList();
var missing = mats.Where(m => !m.HasProperty(e.property)).Select(m => m.name).ToList();
if (missing.Count > 0)
EditorGUILayout.HelpBox(
$"次のマテリアルには {e.property} がありません (シェーダーの更新待ちか名前違い):\n" + string.Join(", ", missing),
MessageType.Error);
// 0% はマテリアルの現在値になるので、0 でないと「スライダー 0% = 0.0」にならない
var nonZero = mats.Where(m => m.HasProperty(e.property) && m.GetFloat(e.property) != 0f).Select(m => m.name).ToList();
if (nonZero.Count > 0)
EditorGUILayout.HelpBox(
$"次のマテリアルの {e.property} が 0 ではありません。スライダー 0% はこの値になります:\n" + string.Join(", ", nonZero),
MessageType.Warning);
}
static void SetField(object obj, string name, object value)
{
obj.GetType().GetField(name).SetValue(obj, value);
}
void Build()
{
var actionType = FindActionType();
if (actionType == null)
{
EditorUtility.DisplayDialog("Albed Sliders", $"VRCFury の {ActionTypeName} が見つかりません。VRCFury のバージョンが違う可能性があります。", "OK");
return;
}
var target = _target;
var entries = _entries.Where(e => e.enabled && !string.IsNullOrEmpty(e.property) && !string.IsNullOrEmpty(e.menuPath)).ToList();
if (entries.Count == 0) return;
// 同じ Menu Path の Toggle が既にあれば、置き換えるかスキップするか選ぶ
var paths = new HashSet<string>(entries.Select(e => e.menuPath.Trim('/', ' ')));
var existing = target.GetComponents<Component>()
.Where(c => BlendShapeToggleBuilder.TryGetToggleMenuPath(c, out var p) && paths.Contains(p)).ToList();
Undo.IncrementCurrentGroup();
Undo.SetCurrentGroupName("Add Albed Sliders");
var skip = new HashSet<string>();
if (existing.Count > 0)
{
var choice = EditorUtility.DisplayDialogComplex("Albed Sliders",
$"'{target.name}' に同じ Menu Path の Toggle が既に {existing.Count} 個あります。", "置き換える", "キャンセル", "既存はそのまま(重複はスキップ)");
if (choice == 1) return;
foreach (var c in existing)
{
BlendShapeToggleBuilder.TryGetToggleMenuPath(c, out var p);
// プレハブ本体側にあるコンポーネントはインスタンスから削除できないのでスキップ扱い
if (choice == 2 || PrefabUtility.GetCorrespondingObjectFromSource(c) != null)
skip.Add(p);
else
Undo.DestroyObjectImmediate(c);
}
}
var latestVersion = actionType.GetMethod("GetLatestVersion").Invoke(Activator.CreateInstance(actionType), null);
var added = new List<string>();
foreach (var e in entries)
{
var path = e.menuPath.Trim('/', ' ');
if (skip.Contains(path)) continue;
var before = new HashSet<Component>(target.GetComponents<Component>());
var toggle = FuryComponents.CreateToggle(target);
toggle.SetMenuPath(path);
toggle.SetSlider();
var vf = target.GetComponents<Component>().First(c => c != null && !before.Contains(c));
Undo.RegisterCreatedObjectUndo(vf, "Add Albed Sliders");
var so = new SerializedObject(vf);
var actions = so.FindProperty("content").FindPropertyRelative("state").FindPropertyRelative("actions");
foreach (var r in _renderers)
{
var action = Activator.CreateInstance(actionType);
actionType.GetProperty("Version").SetValue(action, latestVersion);
SetField(action, "renderer2", r.gameObject);
SetField(action, "affectAllMeshes", false);
SetField(action, "propertyName", e.property);
SetField(action, "value", e.valueAt100); // propertyType は既定の Float
actions.arraySize++;
actions.GetArrayElementAtIndex(actions.arraySize - 1).managedReferenceValue = action;
}
so.FindProperty("content").FindPropertyRelative("defaultSliderValue").floatValue = e.defaultPercent / 100f;
so.ApplyModifiedProperties();
added.Add($"{path} ({e.property})");
}
EditorUtility.SetDirty(target);
EditorSceneManager.MarkSceneDirty(target.scene);
Debug.Log($"[AlbedBrightnessSliderBuilder] '{target.name}' にスライダー Toggle を {added.Count} 個追加しました " +
$"(スキップ {skip.Count} 個 / {_renderers.Count} レンダラー): {string.Join(", ", added)}", target);
}
}

