A Material Property Drawer for the [Curve] attribute which lets you edit curves and adds them to the shader as textures. Forked from Ronja's Material Gradient Drawer. More information here: https://twitter.com/CyrilJGhys/status/1529496742642728964 and here: https://twitter.com/totallyRonja/status/1368704187580682240

June 9, 2022 ยท View on GitHub

using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object;

public class MaterialCurveDrawer : MaterialPropertyDrawer {

private int resolution;

public MaterialCurveDrawer() {
	resolution = 256;
}

public MaterialCurveDrawer(float res) {
	resolution = (int)res;
}

private static bool IsPropertyTypeSuitable(MaterialProperty prop) {
	return prop.type == MaterialProperty.PropType.Texture;
}

public string TextureName(MaterialProperty prop) => $"{prop.name}Tex";

public override void OnGUI(Rect position, MaterialProperty prop, GUIContent label, MaterialEditor editor) {
	if (!IsPropertyTypeSuitable(prop)) {
		EditorGUI.HelpBox(position, $"[Curve] used on non-texture property \"{prop.name}\"", MessageType.Error);
		return;
	}

	if (!AssetDatabase.Contains(prop.targets.FirstOrDefault())) {
		EditorGUI.HelpBox(position, $"Material \"{prop.targets.FirstOrDefault()?.name}\" is not an Asset!",
			MessageType.Error);
		return;
	}

	var textureName = TextureName(prop);

	AnimationCurve currentCurve = null;
	if (prop.targets.Length == 1) {
		var target = (Material) prop.targets[0];
		var path = AssetDatabase.GetAssetPath(target);
		var textureAsset = GetTextureAsset(path, textureName);
		if (textureAsset != null)
			currentCurve = Decode(prop, textureAsset.name);
		if (currentCurve == null)
			currentCurve = new AnimationCurve(){};

		EditorGUI.showMixedValue = false;
	} else {
		EditorGUI.showMixedValue = true;
	}

	using (var changeScope = new EditorGUI.ChangeCheckScope()) {
		currentCurve = EditorGUILayout.CurveField(label, currentCurve, Color.green, new Rect(0f, 0f, 1f, 1f));

		if (changeScope.changed) {
			string encodedCurve = Encode(currentCurve);
			string fullAssetName = textureName + encodedCurve;
			foreach (Object target in prop.targets) {
				if (!AssetDatabase.Contains(target)) //failsafe for non-asset materials - should never trigger
					continue;

				var path = AssetDatabase.GetAssetPath(target);
				var textureAsset = GetTexture(path, textureName);
				Undo.RecordObject(textureAsset, "Change Material Curve");
				textureAsset.name = fullAssetName;
				BakeCurve(currentCurve, textureAsset);
				EditorUtility.SetDirty(textureAsset);
				//we need ImportAsset for changes to show up, but also it costs 100ms per tick and I dont wanna pay that ^^
				//AssetDatabase.ImportAsset(path);

				var material = (Material) target;
				material.SetTexture(prop.name, textureAsset);
			}
		}
	}

	EditorGUI.showMixedValue = false;
}

private Texture2D GetTexture(string path, string name) {
	var textureAsset = GetTextureAsset(path, name);

	if (textureAsset == null) {
		textureAsset = CreateTexture(path, name);
	}

	if (textureAsset.width != resolution) {
		textureAsset.Resize(resolution, 1);
	}

	return textureAsset;
}

private Texture2D CreateTexture(string path, string name = "unnamed texture") {
	//I'm actually unsure about the "no mipchain" thing, mipmapping could also be neat?
	var textureAsset = new Texture2D(resolution, 1, TextureFormat.ARGB32, false);
	textureAsset.wrapMode = TextureWrapMode.Clamp;
	textureAsset.name = name;
	AssetDatabase.AddObjectToAsset(textureAsset, path);
	AssetDatabase.ImportAsset(path);
	return textureAsset;
}

private string Encode(AnimationCurve curve) {
	if (curve == null)
		return null;
	return JsonUtility.ToJson(new CurveRepresentation(curve));
}

private AnimationCurve Decode(MaterialProperty prop, string name) {
	string json = name.Substring(TextureName(prop).Length);
	try { 
		return JsonUtility.FromJson<CurveRepresentation>(json).ToCurve();
	} catch (Exception) {
		return null;
	}
}

private Texture2D GetTextureAsset(string path, string name) {
	return AssetDatabase.LoadAllAssetsAtPath(path).FirstOrDefault(asset => asset.name.StartsWith(name)) as Texture2D;
}

private void BakeCurve(AnimationCurve curve, Texture2D texture) {
	if (curve == null)
		return;
	for (int x = 0; x < texture.width; x++) {
		float value = curve.Evaluate((float)x / (texture.width - 1));
        		Color color = new Color(value, value, value);
		for (int y = 0; y < texture.height; y++) {
			texture.SetPixel(x, y, color);
		}
	}

	texture.Apply();
}

[MenuItem("Assets/Remove All Subassets")]
static void RemoveAllSubassets() {
	foreach (Object asset in Selection.GetFiltered<Object>(SelectionMode.Assets)) {
		var path = AssetDatabase.GetAssetPath(asset);
		foreach (Object subAsset in AssetDatabase.LoadAllAssetRepresentationsAtPath(path)) {
			Object.DestroyImmediate(subAsset, true);
		}
		AssetDatabase.ImportAsset(path);
	}
}

class CurveRepresentation {
	public Key[] keys;

	public CurveRepresentation() { }

	public CurveRepresentation(AnimationCurve source) {
		FromCurve(source);
	}

	public void FromCurve(AnimationCurve source) {
        		keys = source.keys.Select(key => new Key(key)).ToArray();
	}

	public void ToCurve(AnimationCurve curve) {
        		curve.keys = keys.Select(key => key.ToCurveKey()).ToArray();
	}

	public AnimationCurve ToCurve() {
		var curve = new AnimationCurve();
		ToCurve(curve);
		return curve;
	}

	[Serializable]
	public struct Key
	{
	    public float time;
	    public float value;
	    public float inTangent;
	    public float outTangent;
	    public float inWeight;
	    public float outWeight;
	    public WeightedMode weightedMode;

	    public Key(Keyframe source)
	    {
		time = default;
		value = default;
		inTangent = default;
		outTangent = default;
		inWeight = default;
		outWeight = default;
		weightedMode = default;
		FromCurveKey(source);
	    }

	    public void FromCurveKey(Keyframe source)
	    {
		time = source.time;
		value = source.value;
		inTangent = source.inTangent;
		outTangent = source.outTangent;
		inWeight = source.inWeight;
		outWeight = source.outWeight;
		weightedMode = source.weightedMode;
	    }

	    public Keyframe ToCurveKey()
	    {
		Keyframe key = default;
		key.time = time;
		key.value = value;
		key.inTangent = inTangent;
		key.outTangent = outTangent;
		key.inWeight = inWeight;
		key.outWeight = outWeight;
		key.weightedMode = weightedMode;
		return key;
	    }
	}
}

}