Search Unity

Feature Request URP adding a RenderFeature from script

Discussion in 'Universal Render Pipeline' started by andrea_i, May 28, 2021.

  1. andrea_i

    andrea_i

    Joined:
    Nov 18, 2012
    Posts:
    32
    Hi all,

    My tool (clayxels) needs a custom RenderFeature in order to render, I couldn't find the APIs to do that, so I had to hack my way around it like so:
    Code (CSharp):
    1. UnityEngine.Rendering.Universal.ScriptableRendererData forwardPipeData = AssetDatabase.LoadAssetAtPath<UnityEngine.Rendering.Universal.ScriptableRendererData>("Assets/Settings/ForwardRenderer.asset");
    2. List<UnityEngine.Rendering.Universal.ScriptableRendererFeature> passes = forwardPipeData.rendererFeatures;
    3. MyCustomRenderFeature newRenderFeature = ScriptableObject.CreateInstance<MyCustomRenderFeature>();
    4. passes.Add(newRenderFeature);
    5.  
    6. // update the passes list with this hack
    7. MethodInfo dynMethod = forwardPipeData.GetType().GetMethod("OnValidate", BindingFlags.NonPublic | BindingFlags.Instance);
    8. dynMethod.Invoke(forwardPipeData, new object[]{});
    The main problem with this hack (aside from the hack itself) is that when deploying the exe unity will fail to use my renderFeature unless I remove it and add it manually.
    Any short term or long term solution would be super appreciated!
     
    deus0 likes this.
  2. StaggartCreations

    StaggartCreations

    Joined:
    Feb 18, 2015
    Posts:
    2,266
    I'm using a similar function, I'm assuming you're doing this in the editor, not a build? An API would be nice, having to resort to reflection is kind of a red flag :p

    Render features are ScriptableObjects, so they have to be saved to disk in order to remain persistent. This part is missing in your function, it should add the MyCustomRenderFeature instance as a sub-asset to the renderer, get a reference to it, and assign that to the list instead.

    From: https://github.com/Unity-Technologi...l/Editor/ScriptableRendererDataEditor.cs#L180
    Code (CSharp):
    1. AssetDatabase.AddObjectToAsset(newRenderFeature, forwardPipeData);
    2. AssetDatabase.TryGetGUIDAndLocalFileIdentifier(newRenderFeature , out var guid, out long localId);
    This must be called before adding it to the list, otherwise you're not adding a reference to the sub-asset, but rather the "virtual" instance, which would result in "Type mismatch" showing on the field in the inspector (debug mode). And it'll go missing after saving the project.

    If it's an editor-only function, remember to call EditorUtility.SetDirty(forwardPipeData); so things get saved when needed.

    Bonus if you want to fetch the current default renderer, rather than using a hardcoded filepath!
    Code (CSharp):
    1. private static int GetDefaultRendererIndex(UniversalRenderPipelineAsset asset)
    2. {
    3.     return (int)typeof(UniversalRenderPipelineAsset).GetField("m_DefaultRendererIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(asset);
    4. }
    5.  
    6. /// <summary>
    7. /// Gets the renderer from the current pipeline asset that's marked as default
    8. /// </summary>
    9. /// <returns></returns>
    10. public static ScriptableRendererData GetDefaultRenderer()
    11. {
    12.     if (UniversalRenderPipeline.asset)
    13.     {
    14.         ScriptableRendererData[] rendererDataList = (ScriptableRendererData[])typeof(UniversalRenderPipelineAsset)
    15.                 .GetField("m_RendererDataList", BindingFlags.NonPublic | BindingFlags.Instance)
    16.                 .GetValue(UniversalRenderPipeline.asset);
    17.         int defaultRendererIndex = GetDefaultRendererIndex(UniversalRenderPipeline.asset);
    18.  
    19.         return rendererDataList[defaultRendererIndex];
    20.     }
    21.     else
    22.     {
    23.         Debug.LogError("No Universal Render Pipeline is currently active.");
    24.         return null;
    25.     }
    26. }
     
    _geo__, athenspire and deus0 like this.
  3. andrea_i

    andrea_i

    Joined:
    Nov 18, 2012
    Posts:
    32
    StaggartCreations, you saved my day (and my users on URP : )!
    I'll try that straight away, it's great to know there's a workaround while we wait for URP to have these APIs exposed one day, hopefully.
     
  4. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,344
    Sorry for necroing this but two years later and we still have to use reflections, right?

    Here is a class I have written to automatically add renderer features for one of my assets.
    Thanks to @StaggartCreations and @andrea_i

    Code (csharp):
    1.  
    2. using System.Collections.Generic;
    3. using System.Reflection;
    4. using UnityEditor;
    5. using UnityEngine;
    6. using UnityEngine.Rendering;
    7. using UnityEngine.Rendering.Universal;
    8.  
    9. namespace Kamgam
    10. {
    11.     public static class SetupRenderFeatures
    12.     {
    13.         [InitializeOnLoadMethod]
    14.         [MenuItem("Tools/Test/Add Renderer Featues", priority = 10)]
    15.         static void InitOnLoad()
    16.         {
    17.             EditorApplication.delayCall += Setup;
    18.         }
    19.  
    20.         static void Setup()
    21.         {
    22.             addRendererFeature<YourFeatureType>();
    23.         }
    24.  
    25.         static void addRendererFeature<T>() where T : ScriptableRendererFeature
    26.         {
    27.             var handledDataObjects = new List<ScriptableRendererData>();
    28.  
    29.             int levels = QualitySettings.names.Length;
    30.             for (int level = 0; level < levels; level++)
    31.             {
    32.                 // Fetch renderer data
    33.                 var asset = QualitySettings.GetRenderPipelineAssetAt(level) as UniversalRenderPipelineAsset;
    34.                 // Do NOT use asset.LoadBuiltinRendererData().
    35.                 // It's a trap, see: https://github.com/Unity-Technologies/Graphics/blob/b57fcac51bb88e1e589b01e32fd610c991f16de9/Packages/com.unity.render-pipelines.universal/Runtime/Data/UniversalRenderPipelineAsset.cs#L719
    36.                 var data = getDefaultRenderer(asset);
    37.  
    38.                 // This is needed in case multiple renderers share the same renderer data object.
    39.                 // If they do then we only handle it once.
    40.                 if (handledDataObjects.Contains(data))
    41.                 {
    42.                     continue;
    43.                 }
    44.                 handledDataObjects.Add(data);
    45.  
    46.                 // Create & add feature if not yet existing
    47.                 bool found = false;
    48.                 foreach (var feature in data.rendererFeatures)
    49.                 {
    50.                     if (feature is UIToolkitBlurredBackgroundRenderFeatureURP)
    51.                     {
    52.                         found = true;
    53.                         break;
    54.                     }
    55.                 }
    56.                 if (!found)
    57.                 {
    58.                     // Create the feature
    59.                     var feature = ScriptableObject.CreateInstance<T>();
    60.                     feature.name = typeof(T).Name;
    61.  
    62.                     // Add it to the renderer data.
    63.                     addRenderFeature(data, feature);
    64.  
    65.                     Debug.Log("Added render feature '" + feature.name + "' to " + data.name + ". Hope that's okay <3.");
    66.                 }
    67.             }
    68.         }
    69.  
    70.         /// <summary>
    71.         /// Gets the default renderer index.
    72.         /// Thanks to: https://forum.unity.com/threads/urp-adding-a-renderfeature-from-script.1117060/#post-7184455
    73.         /// </summary>
    74.         /// <param name="asset"></param>
    75.         /// <returns></returns>
    76.         static int getDefaultRendererIndex(UniversalRenderPipelineAsset asset)
    77.         {
    78.             return (int)typeof(UniversalRenderPipelineAsset).GetField("m_DefaultRendererIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(asset);
    79.         }
    80.  
    81.         /// <summary>
    82.         /// Gets the renderer from the current pipeline asset that's marked as default.
    83.         /// Thanks to: https://forum.unity.com/threads/urp-adding-a-renderfeature-from-script.1117060/#post-7184455
    84.         /// </summary>
    85.         /// <returns></returns>
    86.         static ScriptableRendererData getDefaultRenderer(UniversalRenderPipelineAsset asset)
    87.         {
    88.             if (asset)
    89.             {
    90.                 ScriptableRendererData[] rendererDataList = (ScriptableRendererData[])typeof(UniversalRenderPipelineAsset)
    91.                         .GetField("m_RendererDataList", BindingFlags.NonPublic | BindingFlags.Instance)
    92.                         .GetValue(asset);
    93.                 int defaultRendererIndex = getDefaultRendererIndex(asset);
    94.  
    95.                 return rendererDataList[defaultRendererIndex];
    96.             }
    97.             else
    98.             {
    99.                 Debug.LogError("No Universal Render Pipeline is currently active.");
    100.                 return null;
    101.             }
    102.         }
    103.  
    104.         /// <summary>
    105.         /// Based on Unity add feature code.
    106.         /// See: AddComponent() in https://github.com/Unity-Technologies/Graphics/blob/d0473769091ff202422ad13b7b764c7b6a7ef0be/com.unity.render-pipelines.universal/Editor/ScriptableRendererDataEditor.cs#180
    107.         /// </summary>
    108.         /// <param name="data"></param>
    109.         /// <param name="feature"></param>
    110.         static void addRenderFeature(ScriptableRendererData data, ScriptableRendererFeature feature)
    111.         {
    112.             // Let's mirror what Unity does.
    113.             var serializedObject = new SerializedObject(data);
    114.  
    115.             var renderFeaturesProp = serializedObject.FindProperty("m_RendererFeatures"); // Let's hope they don't change these.
    116.             var renderFeaturesMapProp = serializedObject.FindProperty("m_RendererFeatureMap");
    117.  
    118.             serializedObject.Update();
    119.  
    120.             // Store this new effect as a sub-asset so we can reference it safely afterwards.
    121.             // Only when we're not dealing with an instantiated asset
    122.             if (EditorUtility.IsPersistent(data))
    123.                 AssetDatabase.AddObjectToAsset(feature, data);
    124.             AssetDatabase.TryGetGUIDAndLocalFileIdentifier(feature, out var guid, out long localId);
    125.  
    126.             // Grow the list first, then add - that's how serialized lists work in Unity
    127.             renderFeaturesProp.arraySize++;
    128.             var componentProp = renderFeaturesProp.GetArrayElementAtIndex(renderFeaturesProp.arraySize - 1);
    129.             componentProp.objectReferenceValue = feature;
    130.  
    131.             // Update GUID Map
    132.             renderFeaturesMapProp.arraySize++;
    133.             var guidProp = renderFeaturesMapProp.GetArrayElementAtIndex(renderFeaturesMapProp.arraySize - 1);
    134.             guidProp.longValue = localId;
    135.  
    136.             // Force save / refresh
    137.             if (EditorUtility.IsPersistent(data))
    138.             {
    139.                 AssetDatabase.SaveAssetIfDirty(data);
    140.             }
    141.  
    142.             serializedObject.ApplyModifiedProperties();
    143.         }
    144.     }
    145. }
    146.  
     
    Last edited: May 12, 2023
    Radivarig likes this.
  5. StaggartCreations

    StaggartCreations

    Joined:
    Feb 18, 2015
    Posts:
    2,266
    I still use reflection wrappers, to access renderers and render features, up to URP v16 even ¯\_(ツ)_/¯

    Plus side is, is that the same functions have been working perfectly throughout every version :D
     
    _geo__ likes this.
  6. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,344
    Haha yes, I too have some assets in the store which have been using reflections for years without any changes :D
     
  7. se

    se

    Joined:
    May 20, 2013
    Posts:
    46
    I'm having trouble adding
    DecalRendererFeature
    (and possibly other annoyingly non-public built-in ones) via editor code. Both variants mentioned here (
    AddVariant1
    and
    AddVariant2
    in my code) work without issues for
    FullScreenPassRendererFeature
    . For
    DecalRendererFeature
    the feature is added as well, but when I have the asset selected in the project window and cause a code recompile I get spammed with the following errors:

    The issue persists a Unity restart and I'm not getting anywhere trying to understand it, so I'd appreciate any help.

    Code (CSharp):
    1. using System;
    2. using System.Reflection;
    3.  
    4. using UnityEditor;
    5. using UnityEngine;
    6. using UnityEngine.Rendering.Universal;
    7.  
    8. public sealed class FluentRenderer : FluentScriptableObject<FluentRenderer, UniversalRendererData>
    9. {
    10.   public FluentRenderer(UniversalRendererData renderer) : base(renderer)
    11.   {
    12.   }
    13.  
    14.   public FluentRenderer() : base()
    15.   {
    16.   }
    17.  
    18.   public FluentRenderer FullScreenPassRendererFeature()
    19.     => Feature(typeof(FullScreenPassRendererFeature));
    20.  
    21.   public FluentRenderer DecalRendererFeature()
    22.     => Feature(Type.GetType("UnityEngine.Rendering.Universal.DecalRendererFeature, Unity.RenderPipelines.Universal.Runtime"));
    23.  
    24.   FluentRenderer Feature(Type type)
    25.   {
    26.     var feature = (ScriptableRendererFeature) ScriptableObject.CreateInstance(type);
    27.     feature.name = type.Name;
    28.  
    29.     AddVariant1(feature);
    30.  
    31.     return this;
    32.   }
    33.  
    34.   void AddVariant1(ScriptableRendererFeature feature)
    35.   {
    36.     AssetUtil.Pack(renderer, feature); // AssetDatabase.AddObjectToAsset and AssetDatabase.SaveAssets
    37.  
    38.     var serializedObject = new SerializedObject(renderer);
    39.  
    40.     var features = serializedObject.FindProperty("m_RendererFeatures");
    41.     var featuresMap = serializedObject.FindProperty("m_RendererFeatureMap");
    42.  
    43.     serializedObject.Update();
    44.  
    45.     features.arraySize++;
    46.  
    47.     features
    48.       .GetArrayElementAtIndex(features.arraySize - 1)
    49.       .objectReferenceValue = feature;
    50.  
    51.     featuresMap.arraySize++;
    52.  
    53.     featuresMap
    54.       .GetArrayElementAtIndex(featuresMap.arraySize - 1)
    55.       .longValue = AssetUtil.GetLocalFileId(feature); // AssetDatabase.TryGetGUIDAndLocalFileIdentifier
    56.  
    57.     serializedObject.ApplyModifiedProperties();
    58.   }
    59.  
    60.   void AddVariant2(ScriptableRendererFeature feature)
    61.   {
    62.     AssetUtil.Pack(renderer, feature); // AssetDatabase.AddObjectToAsset and AssetDatabase.SaveAssets
    63.  
    64.     renderer.rendererFeatures.Add(feature);
    65.  
    66.     renderer
    67.       .GetType()
    68.       .GetMethod("OnValidate", BindingFlags.Instance|BindingFlags.NonPublic)
    69.       .Invoke(renderer, null);
    70.   }
    71. }
     
  8. Radivarig

    Radivarig

    Joined:
    May 15, 2013
    Posts:
    121
    @StaggartCreations on line 33 asset can be null if one of the rp assets for an existing quality level was deleted or unset.
     
    Last edited: Nov 28, 2023