I’m having a bit of an issue with Shader Graph properties. I’m attempting to toggle “Show In Inspector” for Gradient and Sampler State, but they’re not able to be pressed. I was wondering if there’s a solution to this?
I’m in Unity 6000.0.37f1
Unfortunately, Unity doesn’t support exposing these types in a material. There is no existing UI widget for them.
Is there a way to make a custom implementation for this? Hope you’re doing well and I have been loving the tutorials.
Hi,
to add on what @BenCloward said, while Unity does have a Gradient type and widget in C#, there is no gradient type in HLSL, and Shader Graph represents them with a struct, as detailed in the doc.
What we lack at the moment is a ShaderLab property type to link the gradient struct to a Material Editor Gradient UI widget.
Work is under consideration, you can provide feedback using this card.
While doing it for Shader Graph is somehow trivial, we also want to consider VFX Graph, which has its own Gradient property and uses a different implementation. For optimization purposes VFX Graph generates gradients as 1D textures and gathers them in a 2D texture.
What we want in the end is for users to be able to expose a gradient from Shader Graph to both the Material Inspector, but also to VFX Graph, so it can be re-exposed and overridden per instance.
Regarding SampleStates, there’s similar limitation in ShaderLab property types, but also they are not meant to dynamic.
I’m keen to learn about the use cases.
Thanks for your feedback.
Hey,
Thank you so much for the breakdowns it shows a level of depth that I didn’t consider and I really appreciate that.
For Gradients I found that the ability to change per instance is primarily just a nice to have and has just less overhead. Currently I have to bake out gradients outside the engine to them implement for VFX, but having something within the engine lead to faster iteration. It sometimes is difficult maintaining LUTs or Gradients outside the engine that match completely 1:1. It’s a very rare situation, but requires a decent amount of back and forth, from programs such as Photoshop or Substance Designer.
For Sampler States my main use is for them is 2D RGBA Packed textures having different baked textures using different types of States for different effects.
An example of this would be a Repeating Texture on the Red Channel and a Clamped Mask for the Green channel. For testing purposes as well as to create more variety I will also have a Repeating Mask to create more variety. What’s available within the States have what I want, but I just want to be able to change per instance in case the effect requires a certain type of Mask.
Getting these separated feels strange if I want to have one texture be called. I’m not sure if that’s the reality of how a texture being called works.
Again thank you so much for the breakdown, and I hope you’re doing well.
For now, if we just simply want to expose gradients in the material inspector, how could we go about implementing that?
For anyone looking to implement a gradient in the Shader Graph, here is the node setup I am using:
I also vibe coded a scriptable object with a custom inspector to generate such gradient textures:
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "New Gradient", menuName = "Custom/Gradient")]
public class GradientData : ScriptableObject
{
[System.Serializable]
public class GradientPoint
{
public Color color;
[Range(0f, 1f)]
public float position;
public GradientPoint(Color color, float position)
{
this.color = color;
this.position = position;
}
}
public List<GradientPoint> points = new List<GradientPoint>
{
new GradientPoint(Color.black, 0f),
new GradientPoint(Color.white, 1f)
};
[HideInInspector]
public string lastSavedPath = "";
public void SortPoints()
{
points.Sort((a, b) => a.position.CompareTo(b.position));
}
public Color Evaluate(float t)
{
t = Mathf.Clamp01(t);
if (points.Count == 0) return Color.white;
if (points.Count == 1) return points[0].color;
SortPoints();
for (int i = 0; i < points.Count - 1; i++)
{
if (t >= points[i].position && t <= points[i + 1].position)
{
float localT = (t - points[i].position) / (points[i + 1].position - points[i].position);
return Color.Lerp(points[i].color, points[i + 1].color, localT);
}
}
return points[points.Count - 1].color;
}
}
#if UNITY_EDITOR
using UnityEditor;
[CustomEditor(typeof(GradientData))]
public class GradientDataEditor : Editor
{
private GradientData gradient;
private int selectedPointIndex = -1;
private Texture2D previewTexture;
private int imageWidth = 256;
private const float GRADIENT_HEIGHT = 30f;
private const float POINT_SIZE = 10f;
private void OnEnable()
{
gradient = (GradientData)target;
CreatePreviewTexture();
}
private void OnDisable()
{
if (previewTexture != null)
{
DestroyImmediate(previewTexture);
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Gradient Preview", EditorStyles.boldLabel);
DrawGradientPreview();
HandleGradientInput();
// Color picker and position slider for selected point
if (selectedPointIndex >= 0 && selectedPointIndex < gradient.points.Count)
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Selected Point", EditorStyles.boldLabel);
EditorGUI.BeginChangeCheck();
Color newColor = EditorGUILayout.ColorField("Color", gradient.points[selectedPointIndex].color);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(gradient, "Change Point Color");
gradient.points[selectedPointIndex].color = newColor;
EditorUtility.SetDirty(gradient);
UpdatePreviewTexture();
}
EditorGUI.BeginChangeCheck();
float newPosition = EditorGUILayout.Slider("Position", gradient.points[selectedPointIndex].position, 0f, 1f);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(gradient, "Change Point Position");
gradient.points[selectedPointIndex].position = newPosition;
EditorUtility.SetDirty(gradient);
UpdatePreviewTexture();
}
}
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Generate Texture", EditorStyles.boldLabel);
imageWidth = EditorGUILayout.IntField("Image Width", imageWidth);
imageWidth = Mathf.Max(1, imageWidth);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Save"))
{
SaveGradientTexture(false);
}
if (GUILayout.Button("Save As"))
{
SaveGradientTexture(true);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Points", EditorStyles.boldLabel);
if (gradient.points.Count == 0)
{
EditorGUILayout.HelpBox("No gradient points. Double-click the gradient to add points.", MessageType.Info);
}
serializedObject.ApplyModifiedProperties();
}
private void CreatePreviewTexture()
{
if (previewTexture != null)
{
DestroyImmediate(previewTexture);
}
int width = 256;
previewTexture = new Texture2D(width, 1, TextureFormat.RGBA32, false);
previewTexture.wrapMode = TextureWrapMode.Clamp;
UpdatePreviewTexture();
}
private void UpdatePreviewTexture()
{
if (previewTexture == null) return;
int width = previewTexture.width;
Color[] colors = new Color[width];
for (int i = 0; i < width; i++)
{
float t = i / (float)(width - 1);
colors[i] = gradient.Evaluate(t);
}
previewTexture.SetPixels(colors);
previewTexture.Apply();
}
private void DrawGradientPreview()
{
Rect gradientRect = GUILayoutUtility.GetRect(100, GRADIENT_HEIGHT);
if (previewTexture != null)
{
GUI.DrawTexture(gradientRect, previewTexture, ScaleMode.StretchToFill);
}
EditorGUI.DrawRect(new Rect(gradientRect.x, gradientRect.y, gradientRect.width, 1), Color.black);
EditorGUI.DrawRect(new Rect(gradientRect.x, gradientRect.yMax - 1, gradientRect.width, 1), Color.black);
EditorGUI.DrawRect(new Rect(gradientRect.x, gradientRect.y, 1, gradientRect.height), Color.black);
EditorGUI.DrawRect(new Rect(gradientRect.xMax - 1, gradientRect.y, 1, gradientRect.height), Color.black);
for (int i = 0; i < gradient.points.Count; i++)
{
DrawGradientPoint(gradientRect, i);
}
}
private void DrawGradientPoint(Rect gradientRect, int index)
{
var point = gradient.points[index];
float xPos = gradientRect.x + gradientRect.width * point.position;
float yPos = gradientRect.y + gradientRect.height / 2f;
Rect pointRect = new Rect(xPos - POINT_SIZE / 2f, yPos - POINT_SIZE / 2f, POINT_SIZE, POINT_SIZE);
Color pointColor = point.color;
EditorGUI.DrawRect(pointRect, pointColor);
// Draw double border (white then black) for visibility
Rect outerBorder = new Rect(pointRect.x - 2, pointRect.y - 2, pointRect.width + 4, pointRect.height + 4);
EditorGUI.DrawRect(new Rect(outerBorder.x, outerBorder.y, outerBorder.width, 2), Color.white);
EditorGUI.DrawRect(new Rect(outerBorder.x, outerBorder.yMax - 2, outerBorder.width, 2), Color.white);
EditorGUI.DrawRect(new Rect(outerBorder.x, outerBorder.y, 2, outerBorder.height), Color.white);
EditorGUI.DrawRect(new Rect(outerBorder.xMax - 2, outerBorder.y, 2, outerBorder.height), Color.white);
Rect innerBorder = new Rect(pointRect.x - 1, pointRect.y - 1, pointRect.width + 2, pointRect.height + 2);
EditorGUI.DrawRect(new Rect(innerBorder.x, innerBorder.y, innerBorder.width, 1), Color.black);
EditorGUI.DrawRect(new Rect(innerBorder.x, innerBorder.yMax - 1, innerBorder.width, 1), Color.black);
EditorGUI.DrawRect(new Rect(innerBorder.x, innerBorder.y, 1, innerBorder.height), Color.black);
EditorGUI.DrawRect(new Rect(innerBorder.xMax - 1, innerBorder.y, 1, innerBorder.height), Color.black);
// Draw selection indicator
if (index == selectedPointIndex)
{
Rect selectionRect = new Rect(pointRect.x - 3, pointRect.y - 3, pointRect.width + 6, pointRect.height + 6);
EditorGUI.DrawRect(new Rect(selectionRect.x, selectionRect.y, selectionRect.width, 2), Color.yellow);
EditorGUI.DrawRect(new Rect(selectionRect.x, selectionRect.yMax - 2, selectionRect.width, 2), Color.yellow);
EditorGUI.DrawRect(new Rect(selectionRect.x, selectionRect.y, 2, selectionRect.height), Color.yellow);
EditorGUI.DrawRect(new Rect(selectionRect.xMax - 2, selectionRect.y, 2, selectionRect.height), Color.yellow);
}
}
private void HandleGradientInput()
{
Event e = Event.current;
Rect gradientRect = GUILayoutUtility.GetLastRect();
if (gradientRect.Contains(e.mousePosition))
{
if (e.type == EventType.MouseDown && e.button == 0)
{
int clickedPoint = GetPointAtPosition(e.mousePosition, gradientRect);
if (e.clickCount == 2 && clickedPoint == -1)
{
// Double click on empty space - add new point
float t = Mathf.InverseLerp(gradientRect.x, gradientRect.xMax, e.mousePosition.x);
Color color = gradient.Evaluate(t);
Undo.RecordObject(gradient, "Add Gradient Point");
gradient.points.Add(new GradientData.GradientPoint(color, t));
gradient.SortPoints();
selectedPointIndex = gradient.points.FindIndex(p => Mathf.Approximately(p.position, t));
EditorUtility.SetDirty(gradient);
UpdatePreviewTexture();
e.Use();
Repaint();
}
else if (e.clickCount == 1)
{
// Single click - select point or deselect
selectedPointIndex = clickedPoint;
e.Use();
Repaint();
}
}
else if (e.type == EventType.MouseDrag && e.button == 0 && selectedPointIndex != -1)
{
float t = Mathf.InverseLerp(gradientRect.x, gradientRect.xMax, e.mousePosition.x);
t = Mathf.Clamp01(t);
Undo.RecordObject(gradient, "Move Gradient Point");
gradient.points[selectedPointIndex].position = t;
EditorUtility.SetDirty(gradient);
UpdatePreviewTexture();
e.Use();
Repaint();
}
}
if (e.type == EventType.KeyDown && e.keyCode == KeyCode.Delete && selectedPointIndex != -1)
{
if (gradient.points.Count > 1)
{
Undo.RecordObject(gradient, "Delete Gradient Point");
gradient.points.RemoveAt(selectedPointIndex);
selectedPointIndex = -1;
EditorUtility.SetDirty(gradient);
UpdatePreviewTexture();
e.Use();
Repaint();
}
}
}
private int GetPointAtPosition(Vector2 mousePos, Rect gradientRect)
{
for (int i = 0; i < gradient.points.Count; i++)
{
float xPos = gradientRect.x + gradientRect.width * gradient.points[i].position;
float yPos = gradientRect.y + gradientRect.height / 2f;
Rect pointRect = new Rect(xPos - POINT_SIZE / 2f, yPos - POINT_SIZE / 2f, POINT_SIZE, POINT_SIZE);
if (pointRect.Contains(mousePos))
{
return i;
}
}
return -1;
}
private void SaveGradientTexture(bool forceDialog)
{
string path = gradient.lastSavedPath;
// Check if we need to show the dialog
bool needDialog = forceDialog ||
string.IsNullOrEmpty(path) ||
!System.IO.File.Exists(path);
if (needDialog)
{
path = EditorUtility.SaveFilePanelInProject(
"Save Gradient Texture",
gradient.name + "_Gradient.png",
"png",
"Save gradient texture"
);
if (string.IsNullOrEmpty(path))
{
return; // User cancelled
}
// Save the path for future saves
gradient.lastSavedPath = path;
EditorUtility.SetDirty(gradient);
}
// Generate and save the texture
Texture2D texture = new Texture2D(imageWidth, 1, TextureFormat.RGBA32, false);
texture.wrapMode = TextureWrapMode.Clamp;
Color[] colors = new Color[imageWidth];
for (int i = 0; i < imageWidth; i++)
{
float t = i / (float)(imageWidth - 1);
colors[i] = gradient.Evaluate(t);
}
texture.SetPixels(colors);
texture.Apply();
byte[] bytes = texture.EncodeToPNG();
System.IO.File.WriteAllBytes(path, bytes);
AssetDatabase.Refresh();
Debug.Log("Gradient texture saved to: " + path);
DestroyImmediate(texture);
}
}
#endif
Important Note: Do not save the script in a Editor folder, the script already has a preprocessor directive to strip the editor code for production build
Image showing the gradient editor: