├── Media~ └── MaterialTimeline.gif ├── README.md.meta ├── package.json.meta ├── package.json ├── Editor.meta ├── Runtime.meta ├── Runtime ├── Common.meta ├── MaterialTrack.meta ├── RendererTrack.meta ├── material-timeline.asmdef.meta ├── Common │ ├── TextureBlend.shader.meta │ ├── ExtensionMethods.cs.meta │ ├── Texture2DCache.cs.meta │ ├── IMaterialProvider.cs.meta │ ├── RenderTextureCache.cs.meta │ ├── IMaterialProvider.cs │ ├── RenderTextureCache.cs │ ├── Texture2DCache.cs │ ├── TextureBlend.shader │ └── ExtensionMethods.cs ├── MaterialTrack │ ├── MaterialClip.cs.meta │ ├── MaterialMixer.cs.meta │ ├── MaterialTrack.cs.meta │ ├── MaterialBehaviour.cs.meta │ ├── MaterialLayerMixer.cs.meta │ ├── MaterialClip.cs │ ├── MaterialLayerMixer.cs │ ├── MaterialTrack.cs │ ├── MaterialBehaviour.cs │ └── MaterialMixer.cs ├── RendererTrack │ ├── RendererClip.cs.meta │ ├── RendererMixer.cs.meta │ ├── RendererTrack.cs.meta │ ├── RendererBehaviour.cs.meta │ ├── RendererClip.cs │ ├── RendererTrack.cs │ ├── RendererMixer.cs │ └── RendererBehaviour.cs └── material-timeline.asmdef ├── Editor ├── MaterialTrack.meta ├── RendererTrack.meta ├── material-timeline.Editor.asmdef.meta ├── StringTreeView.cs.meta ├── TreeViewPopupWindow.cs.meta ├── RendererTrack │ ├── RendererMixerDrawer.cs.meta │ ├── RendererTrackEditor.cs.meta │ ├── RendererBehaviourDrawer.cs.meta │ ├── RendererTrackEditor.cs │ ├── RendererMixerDrawer.cs │ └── RendererBehaviourDrawer.cs ├── MaterialTrack │ ├── MaterialBehaviourDrawer.cs.meta │ └── MaterialBehaviourDrawer.cs ├── material-timeline.Editor.asmdef ├── StringTreeView.cs └── TreeViewPopupWindow.cs ├── LICENSE~ ├── .gitignore └── README.md /Media~/MaterialTimeline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D4KU/unity-material-timeline/HEAD/Media~/MaterialTimeline.gif -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dd7199779f757e846a8682b869b421fb 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 54ced77470f4a934a9b8541804e0991c 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.d4ku.material-timeline", 3 | "version": "1.0.0", 4 | "displayName": "Material Timeline", 5 | "description": "Use timeline tracks to change material properties" 6 | } 7 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f0c7306abd1cc0242b3f49235e93fac3 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 794f6aba3cd7cef42a9a26fdffd3e814 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Common.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d802b181756c7b44a9e7cdb2519d2183 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/MaterialTrack.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ab4f554798b053b4cac5eaa00b3c7925 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/RendererTrack.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7e523caee3f38bf4eaabbbb54adfe423 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 86246da1e19cc5b43b68b44ddefcde28 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/RendererTrack.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 00e39f6950146fd4b88ff716e7add94b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/material-timeline.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5290d18bad0b2df42a8b58e9df0ed206 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/material-timeline.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fd35aaa4f98914a4485b9572a3035121 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Common/TextureBlend.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bcff8d1170314ea4d86709dbf9bd1796 3 | ShaderImporter: 4 | externalObjects: {} 5 | defaultTextures: [] 6 | nonModifiableTextures: [] 7 | preprocessorOverride: 0 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Editor/StringTreeView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 49a3b9a451e7d6e41868ff26e4e66d1b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/TreeViewPopupWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f3049fc9fea55fc47bb4f2c6709f6a24 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Common/ExtensionMethods.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 014f1151115aa044e8ac239170bfcfd5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Common/Texture2DCache.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a46a3a25a12856144a264d00b15167eb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Common/IMaterialProvider.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c8812e9550c9b5a4386bce3e90579119 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Common/RenderTextureCache.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 527c8397130fc1c46b42ae673121b50c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialClip.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 760992bad9f63824e8345270c4b58f87 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialMixer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5e6e7c48aa0b52049a7baf64e020c3b2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialTrack.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 30f26b4f06e6d2c44ad9a12855570a70 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererClip.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8f42e12d020d00a4a9ad07c5b8622055 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererMixer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 102b70c2634135e4fa8eed04bbfbbd23 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererTrack.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 59b59e9a65777b1499ab9c3b0ad9afbe 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/RendererTrack/RendererMixerDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4e807e5a6ecbd66488066586ce024ed9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/RendererTrack/RendererTrackEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3e3a7fbc4ff6d0b4eae490cdaf997c4e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialBehaviour.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 06709160c67539249a1f77407128d56e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialLayerMixer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 476e341e1be216e42af666db6d5e9877 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererBehaviour.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cfa9d865fbc08c14a9bbf291d956cc47 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/MaterialTrack/MaterialBehaviourDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 128dcbf4d1cf5b747b4363e22857c10a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/RendererTrack/RendererBehaviourDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 866b2472594c46e48b09c91a7a5fb45a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/material-timeline.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-timeline", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:f06555f75b070af458a003d92f9efb00" 6 | ], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialClip.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Playables; 3 | using UnityEngine.Timeline; 4 | 5 | namespace MaterialTrack 6 | { 7 | public class MaterialClip : PlayableAsset, ITimelineClipAsset 8 | { 9 | public MaterialBehaviour template; 10 | public ClipCaps clipCaps => ClipCaps.Extrapolation | ClipCaps.Blending; 11 | 12 | public override Playable CreatePlayable(PlayableGraph graph, GameObject owner) 13 | => ScriptPlayable.Create(graph, template); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererClip.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Playables; 3 | using UnityEngine.Timeline; 4 | 5 | namespace MaterialTrack 6 | { 7 | public class RendererClip : PlayableAsset, ITimelineClipAsset 8 | { 9 | public RendererBehaviour template = new RendererBehaviour(); 10 | public ClipCaps clipCaps => ClipCaps.Extrapolation | ClipCaps.Blending; 11 | 12 | public override Playable CreatePlayable(PlayableGraph graph, GameObject owner) 13 | => ScriptPlayable.Create(graph, template); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Editor/material-timeline.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-timeline.Editor", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:5290d18bad0b2df42a8b58e9df0ed206", 6 | "GUID:02f771204943f4a40949438e873e3eff", 7 | "GUID:f06555f75b070af458a003d92f9efb00" 8 | ], 9 | "includePlatforms": [ 10 | "Editor" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": false, 14 | "overrideReferences": false, 15 | "precompiledReferences": [], 16 | "autoReferenced": true, 17 | "defineConstraints": [], 18 | "versionDefines": [], 19 | "noEngineReferences": false 20 | } -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialLayerMixer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine.Playables; 2 | 3 | namespace MaterialTrack 4 | { 5 | public class MaterialLayerMixer : PlayableBehaviour 6 | { 7 | /// 8 | /// True for the mixer of the first track layer 9 | /// 10 | public static bool frameClean = true; 11 | 12 | public override void ProcessFrame( 13 | Playable playable, 14 | FrameData info, 15 | object playerData) 16 | { 17 | // The layer mixer is executed after all track layer mixers. 18 | // All that is left to do is to tell the first mixer of the next 19 | // frame that it's the first one 20 | MaterialLayerMixer.frameClean = true; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Runtime/Common/IMaterialProvider.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections.Generic; 3 | 4 | namespace MaterialTrack 5 | { 6 | /// 7 | /// An object bound to a track found in this package must be able to 8 | /// provide one ore more materials to operate on. 9 | /// 10 | public interface IMaterialProvider 11 | { 12 | /// 13 | /// The material(s) the timeline track operates on 14 | /// 15 | public IEnumerable Materials { get; } 16 | } 17 | 18 | /// 19 | /// Provides access to data shared between behaviours across one layer 20 | /// 21 | public interface IMixer : IMaterialProvider 22 | { 23 | public RenderTextureCache RenderTextureCache { get; } 24 | public Texture2DCache Texture2DCache { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Editor/RendererTrack/RendererTrackEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Timeline; 3 | using UnityEditor; 4 | using UnityEditor.Timeline; 5 | 6 | namespace MaterialTrack 7 | { 8 | [CustomTimelineEditor(typeof(RendererTrack))] 9 | public class RendererTrackEditor : TrackEditor 10 | { 11 | public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding) 12 | { 13 | var options = base.GetTrackOptions(track, binding); 14 | 15 | // Give the renderer track a better icon. 16 | // Since all renderer subclasses contain an eye in their icon, 17 | // why not use the CanvasRenderer icon that only consists of an eye? 18 | var iconContent = EditorGUIUtility.IconContent("CanvasRenderer Icon"); 19 | options.icon = iconContent.image as Texture2D; 20 | return options; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Runtime/Common/RenderTextureCache.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace MaterialTrack 4 | { 5 | /// 6 | /// Provides a resized to a requested resolution. 7 | /// The same texture is recycled upon each request. 8 | /// 9 | public class RenderTextureCache 10 | { 11 | RenderTexture rt; 12 | 13 | public RenderTexture GetTexture(int width, int height) 14 | { 15 | if (rt == null) 16 | { 17 | rt = new RenderTexture(width, height, 0); 18 | } 19 | else if (width != rt.width || height != rt.height) 20 | { 21 | rt.Release(); 22 | rt.width = width; 23 | rt.height = height; 24 | } 25 | 26 | return rt; 27 | } 28 | 29 | ~RenderTextureCache() 30 | { 31 | if (rt) 32 | rt.SafeDestroy(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Runtime/Common/Texture2DCache.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace MaterialTrack 4 | { 5 | /// 6 | /// Provides a 1x1 in a requested color. 7 | /// The same texture is recycled upon each request. 8 | /// 9 | public class Texture2DCache 10 | { 11 | Texture2D tex; 12 | Color color; 13 | 14 | public Texture2D GetTexture(Color color) 15 | { 16 | bool wasClean = tex == null; 17 | 18 | if (wasClean) 19 | tex = new Texture2D(1, 1) { filterMode = FilterMode.Point }; 20 | 21 | if (wasClean || color != this.color) 22 | { 23 | this.color = color; 24 | tex.SetPixel(0, 0, color); 25 | tex.Apply(updateMipmaps: true, makeNoLongerReadable: true); 26 | } 27 | return tex; 28 | } 29 | 30 | ~Texture2DCache() 31 | { 32 | if (tex) 33 | tex.SafeDestroy(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE~: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 D4KU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Editor/MaterialTrack/MaterialBehaviourDrawer.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace MaterialTrack 5 | { 6 | using T = MaterialBehaviour; 7 | 8 | [CustomPropertyDrawer(typeof(T))] 9 | public class MaterialBehaviourDrawer : RendererBehaviourDrawer 10 | { 11 | public override float GetPropertyHeight( 12 | SerializedProperty property, 13 | GUIContent label) 14 | { 15 | // Material mode needs less space 16 | SerializedProperty useMatP = property.FindPropertyRelative(T.USE_MAT_FIELD); 17 | return useMatP.boolValue ? 0f : base.GetPropertyHeight(property, label); 18 | } 19 | 20 | public override void OnGUI( 21 | Rect position, 22 | SerializedProperty property, 23 | GUIContent label) 24 | { 25 | // Find out if we only lerp whole materials ("material mode") 26 | SerializedProperty useMatP = property.FindPropertyRelative(T.USE_MAT_FIELD); 27 | if (useMatP.boolValue) 28 | { 29 | // Material mode doesn't need all the stuff from base class. 30 | // Just draw a material field. 31 | SerializedProperty matP = property.FindPropertyRelative(T.MAT_FIELD); 32 | EditorGUILayout.PropertyField(matP); 33 | } 34 | else 35 | { 36 | base.OnGUI(position, property, label); 37 | } 38 | 39 | // Draw material mode toggle 40 | EditorGUILayout.PropertyField(useMatP); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialTrack.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Playables; 3 | using UnityEngine.Timeline; 4 | 5 | namespace MaterialTrack 6 | { 7 | [TrackBindingType(typeof(Material))] 8 | [TrackColor(.05f, .6f, .8f)] 9 | [TrackClipType(typeof(MaterialClip))] 10 | public class MaterialTrack : TrackAsset, ILayerable 11 | { 12 | /// 13 | public Playable CreateLayerMixer( 14 | PlayableGraph graph, 15 | GameObject go, 16 | int inputCount) 17 | => ScriptPlayable.Create(graph, inputCount); 18 | 19 | public override Playable CreateTrackMixer( 20 | PlayableGraph graph, GameObject go, int inputCount) 21 | { 22 | var mixer = ScriptPlayable.Create(graph, inputCount); 23 | var behaviour = mixer.GetBehaviour(); 24 | 25 | // Initialize clips 26 | foreach (TimelineClip clip in GetClips()) 27 | { 28 | // Set display name of each clip 29 | var data = ((MaterialClip)clip.asset).template; 30 | clip.displayName = data.materialMode 31 | ? (data.material ? data.material.name : RendererTrack.EMPTY_SLOT_NAME) 32 | : RendererTrack.BuildClipName(data); 33 | 34 | // The track mixer created in this class is the object providing 35 | // each clip's behaviour access to the materials of the bound 36 | // renderer. 37 | data.mixer = behaviour; 38 | } 39 | 40 | return mixer; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Uu]ser[Ss]ettings/ 12 | 13 | # MemoryCaptures can get excessive in size. 14 | # They also could contain extremely sensitive data 15 | /[Mm]emoryCaptures/ 16 | 17 | # Asset meta data should only be ignored when the corresponding asset is also ignored 18 | !/[Aa]ssets/**/*.meta 19 | 20 | # Uncomment this line if you wish to ignore the asset store tools plugin 21 | # /[Aa]ssets/AssetStoreTools* 22 | 23 | # Autogenerated Jetbrains Rider plugin 24 | /[Aa]ssets/Plugins/Editor/JetBrains* 25 | 26 | # Visual Studio cache directory 27 | .vs/ 28 | 29 | # Gradle cache directory 30 | .gradle/ 31 | 32 | # Autogenerated VS/MD/Consulo solution and project files 33 | ExportedObj/ 34 | .consulo/ 35 | *.csproj 36 | *.unityproj 37 | *.sln 38 | *.suo 39 | *.tmp 40 | *.user 41 | *.userprefs 42 | *.pidb 43 | *.booproj 44 | *.svd 45 | *.pdb 46 | *.mdb 47 | *.opendb 48 | *.VC.db 49 | 50 | # Unity3D generated meta files 51 | *.pidb.meta 52 | *.pdb.meta 53 | *.mdb.meta 54 | 55 | # Unity3D generated file on crash reports 56 | sysinfo.txt 57 | 58 | # Builds 59 | *.apk 60 | *.aab 61 | *.unitypackage 62 | 63 | # Crashlytics generated file 64 | crashlytics-build.properties 65 | 66 | # Packed Addressables 67 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 68 | 69 | # Temporary auto-generated Android Assets 70 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 71 | /[Aa]ssets/[Ss]treamingAssets/aa/* -------------------------------------------------------------------------------- /Runtime/Common/TextureBlend.shader: -------------------------------------------------------------------------------- 1 | Shader "Hidden/MaterialTrack/TextureBlend" 2 | { 3 | Properties 4 | { 5 | _MainTex ("Texture", 2D) = "white" {} 6 | [NoScaleOffset] _SideTex ("Texture", 2D) = "white" {} 7 | _Weight ("Weight", Range(0,1)) = 0 8 | } 9 | SubShader 10 | { 11 | Tags { "RenderType"="Opaque" } 12 | LOD 100 13 | 14 | Pass 15 | { 16 | CGPROGRAM 17 | #pragma vertex vert 18 | #pragma fragment frag 19 | 20 | #include "UnityCG.cginc" 21 | 22 | struct appdata 23 | { 24 | float4 vertex : POSITION; 25 | float2 uv : TEXCOORD0; 26 | }; 27 | 28 | struct v2f 29 | { 30 | float2 uv : TEXCOORD0; 31 | float4 vertex : SV_POSITION; 32 | }; 33 | 34 | CBUFFER_START(UnityPerMaterial) 35 | sampler2D _MainTex; 36 | sampler2D _SideTex; 37 | float4 _MainTex_ST; 38 | float _Weight; 39 | CBUFFER_END 40 | 41 | v2f vert (appdata v) 42 | { 43 | v2f o; 44 | o.vertex = UnityObjectToClipPos(v.vertex); 45 | o.uv = TRANSFORM_TEX(v.uv, _MainTex); 46 | return o; 47 | } 48 | 49 | fixed4 frag (v2f i) : SV_Target 50 | { 51 | fixed4 a = tex2D(_MainTex, i.uv); 52 | fixed4 b = tex2D(_SideTex, i.uv); 53 | return (1 - _Weight) * a + _Weight * b; 54 | } 55 | ENDCG 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Editor/StringTreeView.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor.IMGUI.Controls; 2 | using System.Collections.Generic; 3 | using System; 4 | 5 | namespace MaterialTrack 6 | { 7 | /// 8 | /// TreeView to show a list of strings and call a callback when one of them 9 | /// is selected. 10 | /// 11 | public class StringTreeView : TreeView 12 | { 13 | readonly IEnumerable entries; 14 | readonly Action onSelectionChanged; 15 | 16 | public StringTreeView( 17 | IEnumerable entries, 18 | Action onSelectionChanged) 19 | : base(new TreeViewState()) 20 | { 21 | this.entries = entries; 22 | this.onSelectionChanged = onSelectionChanged; 23 | 24 | showAlternatingRowBackgrounds = true; 25 | showBorder = true; 26 | Reload(); 27 | } 28 | 29 | // Called every time Reload is called to ensure that TreeViewItems 30 | // are created from data 31 | protected override TreeViewItem BuildRoot() 32 | { 33 | // IDs should be unique. The root item is required to have a 34 | // depth of -1, and the rest of the items increment from that. 35 | var children = new List(); 36 | var root = new TreeViewItem(id: 0, depth: -1, displayName: "Root"); 37 | 38 | int id = 1; 39 | foreach (string s in entries) 40 | children.Add(new TreeViewItem(id: id++, depth: 0, displayName: s)); 41 | 42 | SetupParentsAndChildrenFromDepths(root, children); 43 | return root; 44 | } 45 | 46 | protected override void SingleClickedItem(int id) 47 | { 48 | TreeViewItem item = FindItem(id, rootItem); 49 | onSelectionChanged(item.displayName); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Editor/TreeViewPopupWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor.IMGUI.Controls; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace MaterialTrack 6 | { 7 | /// 8 | /// Popup that wraps a TreeView element 9 | /// 10 | class TreeViewPopupWindow : PopupWindowContent 11 | { 12 | readonly SearchField searchField; 13 | readonly TreeView treeView; 14 | bool shouldClose; 15 | 16 | public float Width { get; set; } 17 | 18 | public TreeViewPopupWindow(TreeView contents) 19 | { 20 | searchField = new SearchField(); 21 | treeView = contents; 22 | } 23 | 24 | public override void OnGUI(Rect rect) 25 | { 26 | // Escape closes the window 27 | if (shouldClose || 28 | Event.current.type == EventType.KeyDown && 29 | Event.current.keyCode == KeyCode.Escape) 30 | { 31 | GUIUtility.hotControl = 0; 32 | editorWindow.Close(); 33 | GUIUtility.ExitGUI(); 34 | } 35 | 36 | const int BORDER = 4; 37 | const int TOP_PAD = 12; 38 | const int SEARCH_HEIGHT = 20; 39 | const int REMAIN_TOP = TOP_PAD + SEARCH_HEIGHT + BORDER; 40 | var searchRect = new Rect( 41 | BORDER, 42 | TOP_PAD, 43 | rect.width - BORDER * 2, 44 | SEARCH_HEIGHT); 45 | var remainingRect = new Rect( 46 | BORDER, 47 | TOP_PAD + SEARCH_HEIGHT + BORDER, 48 | rect.width - BORDER * 2, 49 | rect.height - REMAIN_TOP - BORDER); 50 | 51 | treeView.searchString = 52 | searchField.OnGUI(searchRect, treeView.searchString); 53 | treeView.OnGUI(remainingRect); 54 | 55 | if (treeView.HasSelection()) 56 | ForceClose(); 57 | } 58 | 59 | public override Vector2 GetWindowSize() 60 | { 61 | var result = base.GetWindowSize(); 62 | result.x = Width; 63 | return result; 64 | } 65 | 66 | public override void OnOpen() 67 | { 68 | searchField.SetFocus(); 69 | base.OnOpen(); 70 | } 71 | 72 | public void ForceClose() => shouldClose = true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererTrack.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Playables; 3 | using UnityEngine.Timeline; 4 | 5 | namespace MaterialTrack 6 | { 7 | [TrackBindingType(typeof(Renderer))] 8 | [TrackColor(.1f, .3f, .7f)] 9 | [TrackClipType(typeof(RendererClip))] 10 | public class RendererTrack : TrackAsset, ILayerable 11 | { 12 | public const string EMPTY_SLOT_NAME = "Empty"; 13 | public RendererMixer template; 14 | 15 | /// 16 | public Playable CreateLayerMixer( 17 | PlayableGraph graph, 18 | GameObject go, 19 | int inputCount) 20 | => ScriptPlayable.Create(graph, inputCount); 21 | 22 | public override Playable CreateTrackMixer( 23 | PlayableGraph graph, GameObject go, int inputCount) 24 | { 25 | // Initialize template 26 | if (this.TryGetBinding(go, out Renderer renderer)) 27 | { 28 | template.boundRenderer = renderer; 29 | ExtensionMethods.ResizeArray( 30 | array: ref template.mask, 31 | newSize: renderer.sharedMaterials.Length, 32 | defaultValue: true); 33 | } 34 | 35 | var mixer = ScriptPlayable.Create(graph, template, inputCount); 36 | var behaviour = mixer.GetBehaviour(); 37 | 38 | // Initialize clips 39 | foreach (TimelineClip clip in GetClips()) 40 | { 41 | // Set display name of each clip 42 | var data = ((RendererClip)clip.asset).template; 43 | clip.displayName = BuildClipName(data); 44 | 45 | // The track mixer created in this class is the object providing 46 | // each clip's behaviour access to the materials of the bound 47 | // renderer. 48 | data.mixer = behaviour; 49 | } 50 | 51 | return mixer; 52 | } 53 | 54 | /// 55 | /// Build string shown on clips from a clip's data 56 | /// 57 | public static string BuildClipName(RendererBehaviour data) 58 | { 59 | if (string.IsNullOrWhiteSpace(data.propertyName)) 60 | return EMPTY_SLOT_NAME; 61 | return $"{data.propertyName} [{data.propertyType}]"; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Editor/RendererTrack/RendererMixerDrawer.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace MaterialTrack 5 | { 6 | [CustomPropertyDrawer(typeof(RendererMixer))] 7 | public class RendererMixerDrawer : PropertyDrawer 8 | { 9 | public override float GetPropertyHeight( 10 | SerializedProperty property, 11 | GUIContent label) 12 | => 0; 13 | 14 | public override void OnGUI( 15 | Rect position, 16 | SerializedProperty property, 17 | GUIContent label) 18 | { 19 | var rendererProp = property.FindPropertyRelative( 20 | nameof(RendererMixer.boundRenderer)); 21 | if (!(rendererProp.objectReferenceValue is Renderer renderer)) 22 | return; 23 | 24 | int slotCount = renderer.sharedMaterials.Length; 25 | var maskProp = property.FindPropertyRelative(nameof(RendererMixer.mask)); 26 | int oldMaskSize = maskProp.arraySize; 27 | 28 | // RendererTrack resizes the mask if this drawer isn't shown, 29 | // and this drawer resizes the mask when the timeline isn't playing 30 | if (oldMaskSize != slotCount) 31 | { 32 | maskProp.arraySize = slotCount; 33 | property.serializedObject.ApplyModifiedPropertiesWithoutUndo(); 34 | } 35 | 36 | EditorGUILayout.LabelField("Slots to affect"); 37 | 38 | // For each available material slot, draw a toggle left from a 39 | // read-only field with the initially assigned material. 40 | for (int i = 0; i < slotCount; i++) 41 | { 42 | SerializedProperty maskElemProp = maskProp.GetArrayElementAtIndex(i); 43 | EditorGUILayout.BeginHorizontal(); 44 | 45 | // Draw toggle 46 | // Set value to true if toggle just got created 47 | maskElemProp.boolValue = EditorGUILayout.Toggle( 48 | value: i >= oldMaskSize | maskElemProp.boolValue, 49 | options: GUILayout.Width(20)); 50 | 51 | // Draw material field 52 | bool guiFormerlyEnabled = GUI.enabled; 53 | GUI.enabled = false; 54 | EditorGUILayout.ObjectField( 55 | obj: renderer.sharedMaterials[i], 56 | objType: typeof(Material), 57 | allowSceneObjects: false); 58 | GUI.enabled = guiFormerlyEnabled; 59 | 60 | EditorGUILayout.EndHorizontal(); 61 | } 62 | 63 | property.serializedObject.ApplyModifiedProperties(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![](https://github.com/D4KU/unity-material-timeline/blob/master/Media%7E/MaterialTimeline.gif) 4 | 5 |
6 | 7 | This is a *Unity Timeline* extension to animate and blend material properties. 8 | It consists of two custom tracks: 9 | 10 | | Track | Description | 11 | | -------------- | ----------- | 12 | | Material Track | Change properties of a material directly, changing it everywhere in the scene. | 13 | | Renderer Track | Overwrite a selection of a renderer's material slots, changing only one specific object.* | 14 | 15 | \* *Material Property Blocks* are used, so instancing isn't broken. 16 | 17 | # Features 18 | 19 | | | Material Track | Renderer Track | 20 | | ---------------------------------- | -------------- | -------------- | 21 | | Layers (a.k.a. Override Tracks) | ✓ | ✓ | 22 | | Clip extrapolation | ✓ | ✓ | 23 | | Set/Blend Float/Range/Color/Vector | ✓ | ✓ | 24 | | Set/Blend* Texture2D/RenderTexture | ✓ | ✓ | 25 | | Set/Blend Texture Tiling/Offset | ✓ | ✓ | 26 | | Set Texture3D | ✓ | ✓ | 27 | | Blend Texture3D | ✗ | ✗ | 28 | | Set CubeMap | ✓ | ✓ | 29 | | Blend CubeMap | ✗ | ✗ | 30 | | Overwrite with entire Material** | ✓ | ✗ | 31 | 32 | All blending can be done between two clips, or with the original value set 33 | in the material. 34 | 35 | \* See [Use texture blending](#use-texture-blending) for how to activate this 36 | feature.
37 | \** Just uses `Material.Lerp` internally, so it's not able to blend textures. 38 | 39 | # Installation 40 | 41 | In your project folder, simply add this to the dependencies inside `Packages/manifest.json`: 42 | 43 | `"com.d4ku.material-timeline": "https://github.com/D4KU/unity-material-timeline.git"` 44 | 45 | Alternatively, you can: 46 | * Clone this repository 47 | * In Unity, go to `Window` > `Package Manager` > `+` > `Add Package from disk` 48 | * Select `package.json` at the root of the package folder 49 | 50 | ## Use texture blending 51 | 52 | This package ships a shader to blend textures, named *TextureBlend*. To tell 53 | Unity to include it in builds, even if no scene has a dependency to it, add it 54 | to the list of always included shaders under *ProjectSettings* > *Graphics*. 55 | Without this shader the package functions normally, but textures are switched 56 | instead of blended. 57 | -------------------------------------------------------------------------------- /Runtime/Common/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.Playables; 4 | using UnityEngine.Rendering; 5 | using UnityEngine.Timeline; 6 | 7 | namespace MaterialTrack 8 | { 9 | public static class ExtensionMethods 10 | { 11 | public static TextureDimension 12 | GetPropertyTextureDimension(this Shader shader, string propertyName) 13 | { 14 | int propIdx = shader.FindPropertyIndex(propertyName); 15 | if (propIdx < 0) 16 | // Shader doesn't have any property with given name 17 | return TextureDimension.Unknown; 18 | return shader.GetPropertyTextureDimension(propIdx); 19 | } 20 | 21 | public static Vector4 GetTextureScaleOffset( 22 | this MaterialPropertyBlock block, string name) 23 | => block.GetVector(name + "_ST"); 24 | 25 | public static void SetTextureScaleOffset( 26 | this MaterialPropertyBlock block, string name, Vector4 value) 27 | => block.SetVector(name + "_ST", value); 28 | 29 | public static Color TextureDefaultNameToColor(this string name) 30 | => name.ToLowerInvariant() switch 31 | { 32 | "black" => Color.black, 33 | "grey" => Color.grey, 34 | "bump" => new Color(.5f, .5f, 1f, 1f), 35 | _ => Color.white, 36 | }; 37 | 38 | public static bool TryGetBinding( 39 | this TrackAsset track, 40 | GameObject owner, 41 | out T binding) where T : class 42 | { 43 | var key = track.isSubTrack ? track.parent : track; 44 | binding = owner.GetComponent() 45 | .GetGenericBinding(key) as T; 46 | return binding != null; 47 | } 48 | 49 | public static void ResizeArray( 50 | ref T[] array, 51 | int newSize, 52 | T defaultValue = default) 53 | { 54 | int oldSize = 0; 55 | if (array == null) 56 | array = new T[newSize]; 57 | else 58 | { 59 | oldSize = array.Length; 60 | if (newSize == oldSize) 61 | return; 62 | Array.Resize(ref array, newSize); 63 | } 64 | 65 | for (int i = Math.Max(oldSize - 1, 0); i < newSize; i++) 66 | array[i] = defaultValue; 67 | } 68 | 69 | /// 70 | /// Destroy working inside and outside Play mode 71 | /// 72 | public static void SafeDestroy(this UnityEngine.Object o) 73 | { 74 | #if UNITY_EDITOR 75 | if (!Application.isPlaying) 76 | UnityEngine.Object.DestroyImmediate(o); 77 | else 78 | #endif 79 | UnityEngine.Object.Destroy(o); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialBehaviour.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System; 3 | using Spt = UnityEngine.Rendering.ShaderPropertyType; 4 | 5 | namespace MaterialTrack 6 | { 7 | [Serializable] 8 | public class MaterialBehaviour : RendererBehaviour 9 | { 10 | public const string USE_MAT_FIELD = nameof(materialMode); 11 | public const string MAT_FIELD = nameof(material); 12 | 13 | [Tooltip("Override all properties of the bound material with the ones " + 14 | "found in this material")] 15 | public Material material; 16 | 17 | [Tooltip("Override all properties of the bound material with the ones " + 18 | "found in another one")] 19 | public bool materialMode; 20 | 21 | public MaterialBehaviour() : base() {} 22 | public MaterialBehaviour(MaterialBehaviour other) : base(other) 23 | { 24 | material = other.material; 25 | materialMode = other.materialMode; 26 | } 27 | 28 | /// 29 | /// Set this behaviour's value from the given material 30 | /// 31 | public override void ApplyFromMaterial(Material source) 32 | { 33 | if (materialMode) 34 | material = new Material(source); 35 | else 36 | base.ApplyFromMaterial(source); 37 | } 38 | 39 | /// 40 | /// Apply this behaviour's value to the passed material 41 | /// 42 | public void ApplyToMaterial(Material target) 43 | { 44 | if (materialMode) 45 | { 46 | if (material && material != target) 47 | target.CopyPropertiesFromMaterial(material); 48 | return; 49 | } 50 | 51 | if (!HasProperty(target)) 52 | return; 53 | 54 | switch (propertyType) 55 | { 56 | case Spt.Float: 57 | case Spt.Range: 58 | target.SetFloat(propertyName, vector.x); 59 | break; 60 | case Spt.Texture: 61 | switch (textureTarget) 62 | { 63 | case TextureTarget.Asset: 64 | ApplyToTexture(target); 65 | break; 66 | case TextureTarget.TilingOffset: 67 | target.SetTextureScale(propertyName, vector); 68 | target.SetTextureOffset( 69 | propertyName, 70 | new Vector2(vector.z, vector.w)); 71 | break; 72 | } 73 | break; 74 | default: 75 | target.SetVector(propertyName, vector); 76 | break; 77 | } 78 | } 79 | 80 | /// 81 | /// Interpret this behaviour's data as texture property data and set it 82 | /// in the given material 83 | /// 84 | void ApplyToTexture(Material target) 85 | { 86 | // Create a 2D texture from the set default color if behaviour has 87 | // no texture set 88 | if (texture == null) 89 | texture = mixer.Texture2DCache.GetTexture(vector); 90 | 91 | if (texture.dimension == 92 | target.shader.GetPropertyTextureDimension(propertyName)) 93 | target.SetTexture(propertyName, texture); 94 | } 95 | 96 | /// 97 | /// Apply the linear interpolation of and 98 | /// to this behaviour 99 | /// 100 | public void Lerp(MaterialBehaviour a, MaterialBehaviour b, float t) 101 | { 102 | if (materialMode) 103 | { 104 | if (material != null && a.material != null && b.material != null) 105 | material.Lerp(a.material, b.material, t); 106 | } 107 | else 108 | { 109 | base.Lerp(a, b, t); 110 | } 111 | } 112 | 113 | /// 114 | public override bool IsBlendableWith(RendererBehaviour other) 115 | { 116 | // If this behaviour is in material mode, it is only blendable 117 | // with another MaterialBehaviour in material mode. 118 | if (materialMode && other is MaterialBehaviour mb && mb.materialMode) 119 | return true; 120 | return base.IsBlendableWith(other); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Runtime/MaterialTrack/MaterialMixer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Playables; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | 6 | namespace MaterialTrack 7 | { 8 | public class MaterialMixer : PlayableBehaviour, IMixer 9 | { 10 | /// 11 | /// Material manipulated by the track 12 | /// 13 | Material boundMaterial; 14 | 15 | /// 16 | /// Material state before timeline initialized 17 | /// 18 | Material defaultMaterial; 19 | 20 | /// 21 | /// Initialization helper 22 | /// 23 | bool firstFrameHappened; 24 | 25 | /// 26 | readonly RenderTextureCache renderTextureCache = new RenderTextureCache(); 27 | 28 | /// 29 | readonly Texture2DCache texture2DCache = new Texture2DCache(); 30 | 31 | /// 32 | public RenderTextureCache RenderTextureCache => renderTextureCache; 33 | 34 | /// 35 | public Texture2DCache Texture2DCache => texture2DCache; 36 | 37 | /// 38 | public IEnumerable Materials => boundMaterial ? 39 | new Material[] { boundMaterial } : new Material[0]; 40 | 41 | public override void OnPlayableDestroy(Playable playable) 42 | { 43 | firstFrameHappened = false; 44 | ResetMaterial(); 45 | } 46 | 47 | void ResetMaterial() 48 | { 49 | // Restore original values 50 | if (boundMaterial && defaultMaterial) 51 | boundMaterial.CopyPropertiesFromMaterial(defaultMaterial); 52 | } 53 | 54 | public override void ProcessFrame( 55 | Playable playable, 56 | FrameData info, 57 | object playerData) 58 | { 59 | boundMaterial = playerData as Material; 60 | if (boundMaterial == null) 61 | return; 62 | 63 | int inputCount = playable.GetInputCount(); 64 | if (inputCount == 0) 65 | return; 66 | 67 | if (MaterialLayerMixer.frameClean) 68 | { 69 | // this mixer is mixing the first track layer 70 | MaterialLayerMixer.frameClean = false; 71 | 72 | if (firstFrameHappened) 73 | { 74 | // Reset bound material 75 | boundMaterial.CopyPropertiesFromMaterial(defaultMaterial); 76 | } 77 | else 78 | { 79 | #if UNITY_EDITOR 80 | // Prevent Unity from saving the previewed version of 81 | // the bound material. Couldn't make it work via 82 | // TrackAsset.GatherProperties(). 83 | UnityEditor.EditorApplication.quitting += ResetMaterial; 84 | #endif 85 | 86 | // Save original value 87 | defaultMaterial = new Material(boundMaterial); 88 | firstFrameHappened = true; 89 | } 90 | } 91 | 92 | // Get clips contributing to the current frame (weight > 0) 93 | List activeClips = Enumerable 94 | .Range(0, inputCount) 95 | .Where(i => playable.GetInputWeight(i) > 0) 96 | .ToList(); 97 | 98 | if (activeClips.Count == 0) 99 | return; 100 | 101 | // The index of the first found active clip of this frame 102 | int clipIndex = activeClips[0]; 103 | 104 | // Data stored in the first active clip 105 | var clipData = GetBehaviour(playable, clipIndex); 106 | 107 | // Weight of the first active clip 108 | float clipWeight = playable.GetInputWeight(clipIndex) * clipData.weightMultiplier; 109 | 110 | // The mixed property value to be applied to the bound material 111 | var mix = new MaterialBehaviour(clipData); 112 | 113 | if (activeClips.Count > 1) 114 | { 115 | // Two clips are blended. 116 | // As long as a valid LayerMixer exists, there can be at most two 117 | // active clips at one specific frame 118 | var next = GetBehaviour(playable, activeClips[1]); 119 | 120 | if (clipData.IsBlendableWith(next)) 121 | { 122 | // Properties of blended clips match. 123 | // Mix current clip with next clip. 124 | mix.Lerp(next, clipData, clipWeight); 125 | } 126 | else 127 | { 128 | // Properties of blended clips don't match. 129 | // Individually mix them them with bound material 130 | 131 | // Next clip 132 | var mix2 = new MaterialBehaviour(next); 133 | mix2.ApplyFromMaterial(boundMaterial); 134 | mix2.Lerp(next, mix2, clipWeight); 135 | mix2.ApplyToMaterial(boundMaterial); 136 | 137 | // Current clip 138 | mix.ApplyFromMaterial(boundMaterial); 139 | mix.Lerp(mix, clipData, clipWeight); 140 | } 141 | } 142 | else if (clipWeight < 1) 143 | { 144 | // The clip blends with the layer background. 145 | // Mix clip with default material. 146 | mix.ApplyFromMaterial(boundMaterial); 147 | mix.Lerp(mix, clipData, clipWeight); 148 | } 149 | 150 | mix.ApplyToMaterial(boundMaterial); 151 | } 152 | 153 | /// 154 | /// Get behaviour at given port from given playable 155 | /// 156 | static MaterialBehaviour GetBehaviour(Playable playable, int inputPort) 157 | => ((ScriptPlayable)playable.GetInput(inputPort)) 158 | .GetBehaviour(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererMixer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Playables; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | 6 | namespace MaterialTrack 7 | { 8 | [System.Serializable] 9 | public class RendererMixer : PlayableBehaviour, IMixer 10 | { 11 | /// 12 | /// Assumed to be of equal length as . 13 | /// Stores for each slot whether to apply a property block to it. 14 | /// 15 | public bool[] mask; 16 | 17 | /// 18 | /// Renderer manipulated by the track 19 | /// 20 | [HideInInspector] public Renderer boundRenderer; 21 | 22 | /// 23 | /// Stores blocks present before the creation of the mixer so it can layer 24 | /// its properties on top and reset them on destruction. One entry per 25 | /// available material. 26 | /// 27 | MaterialPropertyBlock[] initialBlocks; 28 | 29 | /// 30 | /// All materials the bound renderer references 31 | /// 32 | Material[] AvailableMaterials => boundRenderer.sharedMaterials; 33 | 34 | /// 35 | /// Allows to recycle one for texture blending 36 | /// 37 | readonly RenderTextureCache renderTextureCache = new RenderTextureCache(); 38 | 39 | /// 40 | /// Allows to recycle one for color-to-texture 41 | /// conversion 42 | /// 43 | readonly Texture2DCache texture2DCache = new Texture2DCache(); 44 | 45 | /// 46 | public RenderTextureCache RenderTextureCache => renderTextureCache; 47 | 48 | /// 49 | public Texture2DCache Texture2DCache => texture2DCache; 50 | 51 | /// 52 | public IEnumerable Materials => boundRenderer 53 | ? AvailableMaterials.Where((_, i) => mask[i]) 54 | : new Material[0]; 55 | 56 | public override void OnPlayableDestroy(Playable playable) 57 | { 58 | if (boundRenderer) 59 | ResetSlots(); 60 | } 61 | 62 | public override void ProcessFrame( 63 | Playable playable, 64 | FrameData info, 65 | object playerData) 66 | { 67 | if (boundRenderer == null) 68 | return; 69 | 70 | int inputCount = playable.GetInputCount(); 71 | if (inputCount == 0) 72 | return; 73 | 74 | int materialCount = AvailableMaterials.Length; 75 | if (materialCount == 0) 76 | return; 77 | 78 | // Is this mixer mixing the first track layer? 79 | if (MaterialLayerMixer.frameClean) 80 | { 81 | MaterialLayerMixer.frameClean = false; 82 | 83 | // True only in the very first call since creation of the mixer 84 | if (initialBlocks == null) 85 | CacheInitialBlocks(); 86 | else 87 | ResetSlots(); 88 | } 89 | 90 | // Get clips contributing to the current frame (weight > 0) 91 | List activeClips = Enumerable 92 | .Range(0, inputCount) 93 | .Where(i => playable.GetInputWeight(i) > 0) 94 | .ToList(); 95 | 96 | if (activeClips.Count == 0) 97 | return; 98 | 99 | // The index of the first found active clip of this frame 100 | int clipIndex = activeClips[0]; 101 | 102 | // Data stored in the first active clip 103 | RendererBehaviour clipData = GetBehaviour(playable, clipIndex); 104 | 105 | // Weight of the first active clip 106 | float clipWeight = playable.GetInputWeight(clipIndex) * clipData.weightMultiplier; 107 | 108 | // Blocks that will be created during the process 109 | var blocks = new MaterialPropertyBlock[materialCount]; 110 | 111 | for (int slotIndex = 0; slotIndex < materialCount; slotIndex++) 112 | { 113 | if (!mask[slotIndex]) 114 | continue; 115 | 116 | // The property block to apply to the current slot index 117 | var block = new MaterialPropertyBlock(); 118 | 119 | // The mixed property value to apply to the property block at the 120 | // current slot index. 121 | var mix = new RendererBehaviour(clipData); 122 | 123 | // Use blocks already present as starting point and mix to it 124 | boundRenderer.GetPropertyBlock(block, slotIndex); 125 | 126 | // Only consider the per-Renderer block if there is no 127 | // slot-specific one. This mimics Unity's general behaviour: 128 | // the property set of a per-Renderer block isn't extended by an 129 | // index-specific block, it is hidden. 130 | if (block.isEmpty) 131 | boundRenderer.GetPropertyBlock(block); 132 | 133 | if (activeClips.Count > 1) 134 | { 135 | // Two clips are blended. 136 | // As long as a valid LayerMixer exists, there can be at most 137 | // two active clips at one specific frame. 138 | var next = GetBehaviour(playable, activeClips[1]); 139 | 140 | if (clipData.IsBlendableWith(next)) 141 | { 142 | // Properties of blended clips match. 143 | // Mix current clip with next clip. 144 | mix.Lerp(next, clipData, clipWeight); 145 | } 146 | else 147 | { 148 | // Properties of blended clips don't match. 149 | // Individually mix them them with property block 150 | 151 | // Next clip 152 | var mix2 = new RendererBehaviour(next); 153 | ApplyToBehaviour(block, mix2, slotIndex); 154 | mix2.Lerp(next, mix2, clipWeight); 155 | mix2.ApplyToPropertyBlock(block); 156 | 157 | // Current clip 158 | ApplyToBehaviour(block, mix, slotIndex); 159 | mix.Lerp(mix, clipData, clipWeight); 160 | } 161 | } 162 | else if (clipWeight < 1f) 163 | { 164 | // The clip blends with the layer background. 165 | // Mix clip into block. 166 | ApplyToBehaviour(block, mix, slotIndex); 167 | mix.Lerp(mix, clipData, clipWeight); 168 | } 169 | 170 | mix.ApplyToPropertyBlock(block); 171 | boundRenderer.SetPropertyBlock(block, slotIndex); 172 | } 173 | } 174 | 175 | /// 176 | /// Get behaviour at given port from given playable 177 | /// 178 | static RendererBehaviour GetBehaviour(Playable playable, int inputPort) 179 | => ((ScriptPlayable)playable.GetInput(inputPort)) 180 | .GetBehaviour(); 181 | 182 | /// 183 | /// Set the shader property of , with the value 184 | /// taken from if it isn't empty, and from the 185 | /// bound material specified by 186 | /// otherwise. 187 | /// 188 | void ApplyToBehaviour( 189 | MaterialPropertyBlock source, 190 | RendererBehaviour target, 191 | int fallbackMaterialIndex) 192 | { 193 | if (target.ContainsPropertyOf(source)) 194 | target.ApplyFromPropertyBlock(source); 195 | else 196 | { 197 | Material material = AvailableMaterials[fallbackMaterialIndex]; 198 | if (material != null) 199 | target.ApplyFromMaterial(material); 200 | } 201 | } 202 | 203 | /// 204 | /// Stores s present before the 205 | /// creation of the mixer so it can layer its properties on top. 206 | /// 207 | void CacheInitialBlocks() 208 | { 209 | int count = AvailableMaterials.Length; 210 | initialBlocks = new MaterialPropertyBlock[count]; 211 | var block = new MaterialPropertyBlock(); 212 | 213 | for (int i = 0; i < count; i++) 214 | { 215 | boundRenderer.GetPropertyBlock(block, i); 216 | if (block.isEmpty) 217 | continue; 218 | 219 | initialBlocks[i] = block; 220 | block = new MaterialPropertyBlock(); 221 | } 222 | } 223 | 224 | /// 225 | /// Reassign property blocks present before creation of this mixer, 226 | /// delete all others. 227 | /// 228 | void ResetSlots() 229 | { 230 | // In theory we should only reset slots that are not masked out, 231 | // but that proved difficult when user just changed the mask. 232 | // I deemed this not worth the additional code complexity. 233 | int end = AvailableMaterials.Length; 234 | for (int i = 0; i < end; i++) 235 | boundRenderer.SetPropertyBlock(initialBlocks?[i], i); 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Editor/RendererTrack/RendererBehaviourDrawer.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using UnityEngine.Rendering; 4 | using System; 5 | using UnityEditor.Timeline; 6 | using System.Collections.Generic; 7 | using U = UnityEngine.Rendering.ShaderPropertyType; 8 | 9 | namespace MaterialTrack 10 | { 11 | using T = RendererBehaviour; 12 | 13 | [CustomPropertyDrawer(typeof(T))] 14 | public class RendererBehaviourDrawer : PropertyDrawer 15 | { 16 | public override float GetPropertyHeight( 17 | SerializedProperty property, 18 | GUIContent label) 19 | => EditorGUIUtility.singleLineHeight; 20 | 21 | public override void OnGUI( 22 | Rect position, 23 | SerializedProperty property, 24 | GUIContent label) 25 | { 26 | // Draw dropdown to choose a shader property to manipulate 27 | DrawPropertyDropdown(position, property); 28 | SerializedProperty nameProp = property.FindPropertyRelative(T.NAME_FIELD); 29 | 30 | // Draw UI to manipulate the chosen shader property 31 | if (!string.IsNullOrEmpty(nameProp.stringValue)) 32 | { 33 | DrawValueProperty(property); 34 | RefreshObject(property.serializedObject); 35 | } 36 | 37 | EditorGUILayout.PropertyField( 38 | property.FindPropertyRelative(T.WEIGHT_MUL_FIELD)); 39 | } 40 | 41 | /// 42 | /// The label used next to value property fields 43 | /// 44 | protected GUIContent ValueLabel => new GUIContent("Value"); 45 | 46 | /// 47 | /// Based upon the chosen property name in the dropdown, draw the value 48 | /// field of the corresponding type. 49 | /// 50 | protected void DrawValueProperty(SerializedProperty root) 51 | { 52 | SerializedProperty typeP = root.FindPropertyRelative(T.TYPE_FIELD); 53 | SerializedProperty vecP = root.FindPropertyRelative(T.VEC_FIELD); 54 | Vector4 vecVal = vecP.vector4Value; 55 | 56 | // Depending on the chosen shader property type, decide which 57 | // field to draw 58 | switch ((U)typeP.enumValueIndex) 59 | { 60 | case U.Texture: 61 | DrawTextureOptions(root); 62 | break; 63 | case U.Color: 64 | vecP.vector4Value = EditorGUILayout.ColorField( 65 | label: ValueLabel, 66 | value: vecVal, 67 | showEyedropper: true, 68 | showAlpha: true, 69 | hdr: true); 70 | break; 71 | case U.Vector: 72 | EditorGUILayout.PropertyField(vecP, ValueLabel); 73 | break; 74 | case U.Float: 75 | vecVal.x = EditorGUILayout.FloatField(ValueLabel, vecVal.x); 76 | vecP.vector4Value = vecVal; 77 | break; 78 | case U.Range: 79 | vecVal.x = EditorGUILayout.Slider( 80 | label: ValueLabel, 81 | value: vecVal.x, 82 | leftValue: vecVal.y, 83 | rightValue: vecVal.z); 84 | vecP.vector4Value = vecVal; 85 | break; 86 | } 87 | } 88 | 89 | /// 90 | /// Draw an error message box if the chosen shader property doesn't match 91 | /// the given texture dimension 92 | /// 93 | protected void MaybeDrawTextureDimensionErrorBox( 94 | SerializedProperty root, TextureDimension matchTo) 95 | { 96 | // Chosen shader property name 97 | SerializedProperty nameP = root.FindPropertyRelative(T.NAME_FIELD); 98 | 99 | // Draw box if any material this behaviour manipulates has a texture 100 | // property of the same name, with a different dimension. 101 | foreach (Material mat in GetAffectedMaterials(root)) 102 | { 103 | TextureDimension propertyDimension = 104 | mat.shader.GetPropertyTextureDimension(nameP.stringValue); 105 | 106 | if (propertyDimension != matchTo) 107 | { 108 | string msg = "You can't assign a texture with dimension " + 109 | $"{matchTo} to a property with dimension " + 110 | $"{propertyDimension}."; 111 | EditorGUILayout.HelpBox(msg, MessageType.Error); 112 | return; 113 | } 114 | } 115 | } 116 | 117 | /// 118 | /// Draw texture target dropdown and corresponding value field 119 | /// 120 | protected void DrawTextureOptions(SerializedProperty root) 121 | { 122 | // Draw option to manipulate texture asset reference, 123 | // tiling, or offset 124 | SerializedProperty texTrgP = root.FindPropertyRelative(T.TEX_TARGET_FIELD); 125 | 126 | // Draw texture target dropdown button 127 | EditorGUI.BeginChangeCheck(); 128 | EditorGUILayout.PropertyField(texTrgP); 129 | if (EditorGUI.EndChangeCheck()) 130 | { 131 | // New texture target was chosen. 132 | // Apply default values from first affected material. 133 | T target = GetTarget(root); 134 | foreach (Material mat in GetAffectedMaterials(target)) 135 | { 136 | RefreshObject(root.serializedObject); 137 | target.ApplyFromMaterial(mat); 138 | break; 139 | } 140 | } 141 | 142 | if (texTrgP.enumValueIndex == (int)T.TextureTarget.Asset) 143 | { 144 | // If asset reference is chosen, draw a texture reference field 145 | DrawTextureAssetField(root); 146 | } 147 | else 148 | { 149 | // Otherwise, draw a Vector4 for Tiling or Offset 150 | SerializedProperty vecP = root.FindPropertyRelative(T.VEC_FIELD); 151 | 152 | // Ensure to not overwrite z and w components of 'vecP' 153 | // vecP.vector4Value = EditorGUILayout.Vector4Field(ValueLabel, vecP.vector4Value); 154 | 155 | Rect rect = EditorGUILayout.GetControlRect( 156 | hasLabel: true, 157 | height: EditorGUIUtility.singleLineHeight * 2 + EditorGUIUtility.standardVerticalSpacing); 158 | vecP.vector4Value = MaterialEditor.TextureScaleOffsetProperty(rect, vecP.vector4Value); 159 | } 160 | } 161 | 162 | /// 163 | /// Draw texture value field with a field for the default color 164 | /// 165 | protected void DrawTextureAssetField(SerializedProperty root) 166 | { 167 | SerializedProperty texP = root.FindPropertyRelative(T.TEX_FIELD); 168 | EditorGUILayout.PropertyField(texP, new GUIContent(texP.displayName, texP.tooltip)); 169 | 170 | // The UI would be nicer here if we would render an object field 171 | // of the Texture subclass the chosen property actually expects. 172 | // The problem are RenderTextures, which don't inherit from 173 | // Texture2D, but are indistinguishable from them for shaders. 174 | // So instead, all we can do is to render a Texture base class field 175 | // and display an error when the linked texture dimension is 176 | // incorrect. 177 | if (texP.objectReferenceValue is Texture texture) 178 | MaybeDrawTextureDimensionErrorBox(root, texture.dimension); 179 | 180 | SerializedProperty vecP = root.FindPropertyRelative(T.VEC_FIELD); 181 | GUIContent defLabel = new GUIContent("Default Color", "When this " + 182 | "texture is blended and no other clip is available to " + 183 | "supply the second texture, the texture is blended with " + 184 | "this color instead. It is also used if no texture is set."); 185 | vecP.vector4Value = EditorGUILayout.ColorField(defLabel, vecP.vector4Value); 186 | } 187 | 188 | /// 189 | /// Get the currently drawn behaviour 190 | /// 191 | protected T GetTarget(SerializedProperty root) 192 | { 193 | // Object whose inspector is currently drawn. 194 | UnityEngine.Object targetObject = root.serializedObject.targetObject; 195 | return fieldInfo.GetValue(targetObject) as T; 196 | } 197 | 198 | /// 199 | /// Returns the materials the given behaviour manipulates. 200 | /// Never returns null. 201 | /// 202 | protected IEnumerable GetAffectedMaterials(T target) 203 | { 204 | if (target?.Materials != null) 205 | return target.Materials; 206 | 207 | // Ensure that the timeline rebuilds the graph, so that 208 | // the material provider could initialize 209 | TimelineEditor.Refresh(RefreshReason.ContentsModified); 210 | return new Material[0]; 211 | } 212 | 213 | /// 214 | /// Returns the materials the currently drawn behaviour manipulates. 215 | /// Never returns null. 216 | /// 217 | protected IEnumerable GetAffectedMaterials(SerializedProperty root) 218 | => GetAffectedMaterials(GetTarget(root)); 219 | 220 | /// 221 | /// Draw a searchable dropdown from which to chose the name of the shader 222 | /// property to manipulate 223 | /// 224 | protected void DrawPropertyDropdown(Rect position, SerializedProperty root) 225 | { 226 | // The currently chosen shader property name 227 | SerializedProperty nameProp = root.FindPropertyRelative(T.NAME_FIELD); 228 | 229 | // Draw dropdown button 230 | Rect dropdownLabel = EditorGUI.PrefixLabel( 231 | new Rect( 232 | position.x, 233 | position.y, 234 | position.width, 235 | EditorGUIUtility.singleLineHeight), 236 | new GUIContent("Property")); 237 | bool dropdownOpen = EditorGUI.DropdownButton( 238 | dropdownLabel, 239 | new GUIContent(nameProp.stringValue), 240 | FocusType.Keyboard); 241 | 242 | if (dropdownOpen) 243 | DrawMaterialPropertyList(dropdownLabel, root); 244 | } 245 | 246 | /// 247 | /// Draw a searchable list of shader properties found in the given materials 248 | /// 249 | protected void DrawMaterialPropertyList(Rect position, SerializedProperty root) 250 | { 251 | // Object this drawer renders. It's a field of 'targetObject'. 252 | T target = GetTarget(root); 253 | 254 | // Materials to list properties of 255 | IEnumerable materials = GetAffectedMaterials(target); 256 | 257 | // Collect all unique property names to fill list with 258 | var propertyNames = new HashSet(); 259 | foreach (Material mat in materials) 260 | { 261 | // Can't pass all materials directly to GetMaterialProperties(), 262 | // because it demands that they share all properties 263 | var props = MaterialEditor.GetMaterialProperties( 264 | new Material[] { mat }); 265 | 266 | foreach (MaterialProperty p in props) 267 | propertyNames.Add(p.name); 268 | } 269 | 270 | // Create callback function when dropdown entry got selected 271 | Action OnSelectionChanged = entry => 272 | { 273 | // Choose the first material that has the selected property 274 | // to retrieve the corresponding shader property type 275 | foreach (Material mat in materials) 276 | { 277 | Shader shader = mat.shader; 278 | int propIndex = shader.FindPropertyIndex(entry); 279 | if (propIndex < 0) 280 | // Shader doesn't have any property with selected name 281 | continue; 282 | 283 | target.propertyName = entry; 284 | target.propertyType = shader.GetPropertyType(propIndex); 285 | target.ApplyFromMaterial(mat); 286 | 287 | // Ensure selected entry is triggering updates immediately 288 | RefreshObject(root.serializedObject); 289 | TimelineEditor.Refresh(RefreshReason.ContentsModified); 290 | return; 291 | } 292 | }; 293 | 294 | // Build dropdown popup and show it 295 | var treeView = new StringTreeView(propertyNames, OnSelectionChanged); 296 | var treeViewPopup = new TreeViewPopupWindow(treeView) 297 | { 298 | Width = position.width 299 | }; 300 | PopupWindow.Show(position, treeViewPopup); 301 | } 302 | 303 | protected void RefreshObject(SerializedObject toRefresh) 304 | { 305 | toRefresh.ApplyModifiedProperties(); 306 | toRefresh.Update(); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /Runtime/RendererTrack/RendererBehaviour.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Playables; 3 | using UnityEngine.Rendering; 4 | using System; 5 | using System.Collections.Generic; 6 | using Spt = UnityEngine.Rendering.ShaderPropertyType; 7 | 8 | namespace MaterialTrack 9 | { 10 | /// 11 | /// The data container of each clip. It can be seen as a tagged union, 12 | /// where the tag is the shader property name of the active value. Because 13 | /// a shader property can have different types, there are several value 14 | /// fields, but only one field is considered active at a time. 15 | /// 16 | [Serializable] 17 | public class RendererBehaviour : PlayableBehaviour, IMaterialProvider 18 | { 19 | /// Used for serialization in this class's inspector drawer 20 | public const string TYPE_FIELD = nameof(propertyType); 21 | public const string NAME_FIELD = nameof(propertyName); 22 | public const string WEIGHT_MUL_FIELD = nameof(weightMultiplier); 23 | public const string TEX_FIELD = nameof(texture); 24 | public const string VEC_FIELD = nameof(vector); 25 | public const string TEX_TARGET_FIELD = nameof(textureTarget); 26 | 27 | /// 28 | /// Specifies how a texture is manipulated 29 | /// 30 | public enum TextureTarget 31 | { 32 | Asset, 33 | TilingOffset 34 | } 35 | 36 | /// 37 | /// Mixer Behaviour of the track the clip of this behaviour is in. 38 | /// Set on instantiation. 39 | /// 40 | public IMixer mixer; 41 | 42 | [Tooltip("Name of the shader property to manipulate")] 43 | public string propertyName = ""; 44 | 45 | [Tooltip("Type of the shader property to manipulate")] 46 | public Spt propertyType = Spt.Float; 47 | 48 | [Range(0, 1)] 49 | [Tooltip("Blend between clip value and underlying material value")] 50 | public float weightMultiplier = 1; 51 | 52 | [Tooltip("Texture to assign to the shader property")] 53 | public Texture texture; 54 | 55 | [Tooltip("Value to assign to the shader property")] 56 | public Vector4 vector; 57 | 58 | [Tooltip("How to manipulate the texture?")] 59 | public TextureTarget textureTarget; 60 | 61 | /// 62 | public IEnumerable Materials => mixer?.Materials; 63 | 64 | /// To not search for a shader every frame that we'll never find. 65 | /// Only considered in builds. 66 | #if !UNITY_EDITOR 67 | static bool triedFindShader; 68 | #endif 69 | 70 | /// 71 | static Material blendMaterial; 72 | 73 | /// Material used to blend textures 74 | public static Material BlendMaterial 75 | { 76 | get 77 | { 78 | if (blendMaterial == null 79 | #if !UNITY_EDITOR 80 | && !triedFindShader 81 | #endif 82 | ) 83 | { 84 | Shader shader = Shader.Find("Hidden/MaterialTrack/TextureBlend"); 85 | if (shader == null) 86 | Debug.LogWarning("'TextureBlend' shader could not be found. " + 87 | "To ensure it's included in the build, add it to the " + 88 | "list of always included shaders under ProjectSettings " + 89 | "> Graphics."); 90 | else 91 | blendMaterial = new Material(shader); 92 | #if !UNITY_EDITOR 93 | triedFindShader = true; 94 | #endif 95 | } 96 | return blendMaterial; 97 | } 98 | } 99 | 100 | public RendererBehaviour() : base() {} 101 | public RendererBehaviour(RendererBehaviour other) : base() 102 | { 103 | propertyName = other.propertyName; 104 | propertyType = other.propertyType; 105 | texture = other.texture; 106 | vector = other.vector; 107 | textureTarget = other.textureTarget; 108 | mixer = other.mixer; 109 | } 110 | 111 | protected bool HasProperty(Material material) 112 | => material.HasProperty(Shader.PropertyToID(propertyName)); 113 | 114 | /// 115 | /// Apply the linear interpolation of and 116 | /// to this behaviour 117 | /// 118 | public virtual void Lerp(RendererBehaviour a, RendererBehaviour b, float t) 119 | { 120 | // Lerp vector even if target is a texture asset, because it is then 121 | // used to store the texture default value. 122 | vector = Vector4.Lerp(a.vector, b.vector, t); 123 | 124 | if (propertyType != Spt.Texture || textureTarget != TextureTarget.Asset) 125 | return; 126 | 127 | var cache = mixer.Texture2DCache; 128 | 129 | // Blend colors of behaviours and bake the result into a texture 130 | // if both blended clips have no texture assigned 131 | if (a.texture == null && b.texture == null) 132 | { 133 | texture = cache.GetTexture(vector); 134 | return; 135 | } 136 | 137 | // Blend textures. If one clip has no texture assigned, mix its 138 | // color with the texture of the other one. 139 | texture = BlendTextures( 140 | a: a.texture ? a.texture : cache.GetTexture(a.vector), 141 | b: b.texture ? b.texture : cache.GetTexture(b.vector), 142 | t: t); 143 | } 144 | 145 | /// 146 | /// Set this behaviour's value from the given material property block 147 | /// 148 | public void ApplyFromPropertyBlock(MaterialPropertyBlock source) 149 | { 150 | switch (propertyType) 151 | { 152 | case Spt.Float: 153 | case Spt.Range: 154 | vector.x = source.GetFloat(propertyName); 155 | break; 156 | case Spt.Texture: 157 | switch (textureTarget) 158 | { 159 | case TextureTarget.TilingOffset: 160 | vector = source.GetTextureScaleOffset(propertyName); 161 | break; 162 | default: 163 | texture = source.GetTexture(propertyName); 164 | break; 165 | } 166 | break; 167 | default: 168 | vector = source.GetVector(propertyName); 169 | break; 170 | } 171 | } 172 | 173 | /// 174 | /// Returns true if the given property block contains the property set 175 | /// in this behaviour 176 | /// 177 | public bool ContainsPropertyOf(MaterialPropertyBlock block) 178 | { 179 | #if UNITY_2021_1_OR_NEWER 180 | return block.HasProperty(propertyName); 181 | #else 182 | switch (propertyType) 183 | { 184 | case Spt.Float: 185 | case Spt.Range: 186 | return block.GetFloat(propertyName) != 0f; 187 | case Spt.Texture: 188 | switch (textureTarget) 189 | { 190 | case TextureTarget.TilingOffset: 191 | return block.GetTextureScaleOffset(propertyName) != 192 | Vector4.zero; 193 | default: 194 | return block.GetTexture(propertyName) != null; 195 | } 196 | default: 197 | return block.GetVector(propertyName) != Vector4.zero; 198 | } 199 | #endif 200 | } 201 | 202 | /// 203 | /// Apply this behaviour's value to the given material property block 204 | /// 205 | public void ApplyToPropertyBlock(MaterialPropertyBlock target) 206 | { 207 | switch (propertyType) 208 | { 209 | case Spt.Float: 210 | case Spt.Range: 211 | target.SetFloat(propertyName, vector.x); 212 | break; 213 | case Spt.Texture: 214 | switch (textureTarget) 215 | { 216 | case TextureTarget.Asset: 217 | if (texture == null) 218 | texture = mixer.Texture2DCache.GetTexture(vector); 219 | target.SetTexture(propertyName, texture); 220 | break; 221 | case TextureTarget.TilingOffset: 222 | target.SetTextureScaleOffset(propertyName, vector); 223 | break; 224 | } 225 | break; 226 | case Spt.Color: 227 | // Use SetColor() to apply gamma correction in HDRP 228 | target.SetColor(propertyName, vector); 229 | break; 230 | default: 231 | target.SetVector(propertyName, vector); 232 | break; 233 | } 234 | } 235 | 236 | /// 237 | /// Set this behaviour's value from the given material 238 | /// 239 | public virtual void ApplyFromMaterial(Material source) 240 | { 241 | if (!HasProperty(source)) 242 | return; 243 | 244 | Shader shader = source.shader; 245 | int propIndex = shader.FindPropertyIndex(propertyName); 246 | switch (propertyType) 247 | { 248 | case Spt.Float: 249 | vector.x = source.GetFloat(propertyName); 250 | break; 251 | case Spt.Range: 252 | Vector2 limits = shader.GetPropertyRangeLimits(propIndex); 253 | 254 | // Pack range limits into unused vector components 255 | vector.x = source.GetFloat(propertyName); 256 | vector.y = limits.x; 257 | vector.z = limits.y; 258 | break; 259 | case Spt.Texture: 260 | switch (textureTarget) 261 | { 262 | case TextureTarget.Asset: 263 | texture = source.GetTexture(propertyName); 264 | // default is a white texture mode 265 | vector = shader 266 | .GetPropertyTextureDefaultName(propIndex) 267 | .TextureDefaultNameToColor(); 268 | break; 269 | case TextureTarget.TilingOffset: 270 | Vector2 scale = source.GetTextureScale(propertyName); 271 | Vector2 offset = source.GetTextureOffset(propertyName); 272 | vector = new Vector4(scale.x, scale.y, offset.x, offset.y); 273 | break; 274 | } 275 | break; 276 | default: 277 | vector = source.GetVector(propertyName); 278 | break; 279 | } 280 | } 281 | 282 | /// 283 | /// Create a new texture as linear interpolation of the given textures. 284 | /// Resolution is also interpolated. 285 | /// The first texture passed determines the output texture's non-blendable 286 | /// properties, such as wrapMode. 287 | /// 288 | protected Texture BlendTextures(Texture a, Texture b, float t) 289 | { 290 | if (a.dimension != TextureDimension.Tex2D || 291 | b.dimension != TextureDimension.Tex2D || 292 | BlendMaterial == null) 293 | { 294 | // Blend failed, resort to hard cut 295 | return t < .5 ? a : b; 296 | } 297 | 298 | // Set 'b' and 't' in material for blending. 299 | // 'a' is set by Graphics.Blit() to the '_MaintTex' property. 300 | blendMaterial.SetTexture("_SideTex", b); 301 | blendMaterial.SetFloat("_Weight", t); 302 | 303 | RenderTexture mix = mixer.RenderTextureCache.GetTexture( 304 | width: (int)Mathf.Lerp(a.width, b.width, t), 305 | height: (int)Mathf.Lerp(a.height, b.height, t)); 306 | 307 | mix.anisoLevel = Mathf.CeilToInt(Mathf.Lerp( a.anisoLevel, b.anisoLevel, t)); 308 | mix.filterMode = (FilterMode)Mathf.CeilToInt(Mathf.Lerp((int)a.filterMode, (int)b.filterMode, t)); 309 | mix.wrapMode = a.wrapMode; 310 | 311 | // Render blend of both given textures to render texture. 312 | Graphics.Blit(a, mix, blendMaterial); 313 | return mix; 314 | } 315 | 316 | /// 317 | /// Returns true if it is possible to lerp between this and the given 318 | /// behaviour. 319 | /// 320 | public virtual bool IsBlendableWith(RendererBehaviour other) 321 | => other != null 322 | && propertyName == other.propertyName 323 | && propertyType == other.propertyType 324 | && (propertyType != Spt.Texture || textureTarget == other.textureTarget); 325 | // If two behaviours aren't textures, they are blendable if their 326 | // property names and types match. If they are textures, they must 327 | // also have identical texture targets. 328 | } 329 | } 330 | --------------------------------------------------------------------------------