-
-
Save ghysc/b4f9b3266ee82edf2b02e00cef0bc6b7 to your computer and use it in GitHub Desktop.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment