Jump to content

[Workaround Found] Re-initialize part highlighting in editor after model changes


Recommended Posts

Hi all,

While working with some of my partModules that deal with procedural models (and/or model-swapping), I've found a bit of a bug with the stock highlighting code, and not sure how best to work around it.

Anytime I change the model components of a part after it has been first initialized (e.g. if I swap models in the editor, or recreate a procedural model in the editor, or just add a new additional model to the part) the mouse-over and stage highlighting for the part stop working for the newly added/changed parts of the model. If I somehow keep the same mesh-renderer for the entire lifetime, and merely swap meshes, the highlighting continues to work.  However, there is no easy/clean way to fully replace a complex hierarchical model while re-using existing mesh-renderers.

 

So, I suppose the question is this -- how do I tell the stock code to re-initialize the highlighter for a part / tell it to update its list of mesh-renderers that it should highlight?  I've tried part.highlighter.ReInitMaterials(), with no effect.  I've tried deleting the existing highlighter component and adding a new one; still did not work.

The only other information I could find regarding highlighting on the forums was a snippet from the 0.2.x-0.9 upgrade thread (http://forum.kerbalspaceprogram.com/index.php?/topic/93010-whats-new-in-090/&page=2#comment-1615674) but that did not seem to have any new information in it.

 

Thanks in advance for any information,

Shadowmage

Edit:

Workaround code is below:

using UnityEngine;
using System.Collections.Generic;

namespace SSTUTools
{
    public class SSTUHighlightFix : PartModule
    {

        [KSPField]
        public string transformName = "HighlightingHackObject";

        private Transform dummyTransform;

        private Renderer[] cachedRenderList;

        private MaterialPropertyBlock mpb;

        private static int colorID;
        private static int falloffID;

        public override void OnStart(StartState state)
        {
            base.OnStart(state);
            MonoBehaviour.print("Starting highlighting fixer for part: " + part.name);
            dummyTransform = part.transform.FindRecursive(transformName);
            if (dummyTransform == null)//should only be null on the prefab part
            {
                GameObject newObj = new GameObject(transformName);
                newObj.transform.name = transformName;
                newObj.transform.NestToParent(part.transform.FindRecursive("model"));

                Renderer render = newObj.AddComponent<MeshRenderer>();//add a new render
                render.material = SSTUUtils.loadMaterial(null, null);//with an empty dummy material, also it doesn't actually have any mesh
                dummyTransform = newObj.transform;//cache reference to it for use for updating
            }
            if (HighLogic.LoadedSceneIsEditor)
            {
                GameEvents.onEditorShipModified.Add(new EventData<ShipConstruct>.OnEvent(onEditorVesselModified));
            }
        }

        public void Start()
        {
            colorID = HighLogic.ShaderPropertyID_RimColor;
            falloffID = HighLogic.ShaderPropertyID_RimFalloff;
            mpb = new MaterialPropertyBlock();
            updateRenderCache();
        }

        /// <summary>
        /// Event callback for when vessel is modified in the editor.  Used in this case to update the cached render list (my modular parts always call onEditorVesselModified when their models are changed, so this is a good enough catch for them)
        /// </summary>
        /// <param name="ship"></param>
        public void onEditorVesselModified(ShipConstruct ship)
        {
            if (!HighLogic.LoadedSceneIsEditor) { return; }
            updateRenderCache();
        }

        public void LateUpdate()
        {
            if (HighLogic.LoadedSceneIsEditor)
            {
                Color color = dummyTransform.renderer.material.GetColor(colorID);
                float falloff = dummyTransform.renderer.material.GetFloat(falloffID);

                mpb.SetColor(colorID, color);
                mpb.SetFloat(falloffID, falloff);
                bool updateCache = false;
                int len = cachedRenderList.Length;
                for (int i = 0; i < len; i++)
                {
                    //somehow we got a nulled out render, object was likely deleted -- update the cached list, will be correct for the next tick/update cycle
                    if (cachedRenderList[i] == null)
                    {
                        updateCache = true;
                        continue;
                    }
                    cachedRenderList[i].SetPropertyBlock(mpb);
                }
                if (updateCache)
                {
                    updateRenderCache();
                }
            }
        }

        private void updateRenderCache()
        {
            cachedRenderList = null;
            Renderer[] renders = part.transform.GetComponentsInChildren<Renderer>(true);
            List<Renderer> rendersToCache = new List<Renderer>();
            int len = renders.Length;
            for (int i = 0; i < len; i++)
            {
                //skip the dummy renderer; though it honestly should not matter if it is in the list or not, as we are pulling the current vals from it before setting anything
                if (renders[i].transform != dummyTransform)
                {
                    rendersToCache.Add(renders[i]);
                }
            }
            cachedRenderList = rendersToCache.ToArray();
        }

    }
}

 

Edited by Shadowmage
Link to comment
Share on other sites

  • 1 month later...

Have found how to potentially solve this problem.  The real problem?  It is all private fields that need to be manipulated, without any proper access available:

 

Here is the hacky solution -- null out the renderer list and the part will re-initialize it next time it attempts to be highlighted:

(Disclaimer, don't actually do this / use this code...for various reasons)

MonoBehaviour.print("updating part highlighter for: " + part);
FieldInfo[] fi = typeof(Part).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
foreach (FieldInfo f in fi)
{
    if (f.FieldType == typeof(List<Renderer>)) // somewhere Part has a list of renderers that it uses as a cache for the renderers that should be highlighted.
    {
        f.SetValue(part, null);
    }
}

(No, I will not be publishing my mod with the above code; merely posting it as an academic example of the solution and the problems with the solution)

 

@NathanKell  Any chance of getting proper access to the part cached renderer list, in order to clear it to re-initialize the part highlighting properly?  (even just a method to re-initialize the list, or even some internal code that auto-re-initializes the list when the render changes).  (Again, sorry to bug you, but unsure where else to turn that has any chance of getting things fixed; and again, feel free to ignore me or just tell me no :)).

90% of my modular parts have problems with the current highlighting code while in the editer -- as they change models after the part is initialized (by instantiating entirely new models), these new models never get re-cached/added to the cached render list, and are thus never highlighted.

 

 

 

 

 

 

 

 

 

Link to comment
Share on other sites

6 hours ago, sarbian said:

Do you try to set part.hasHeiarchyModel to true ?

I have not messed with that variable at all (assumed it was auto set from when the models were added to the part, and used for some other internal use).

Will investigate tonight to see if it has any effect.

Link to comment
Share on other sites

Apparently the parts are already set with 'hasHierarchyModel = true' (or at least that is what it prints while debugging).

Toggling that flag between true/false had no effect on highlighting though.

Ohwell, thanks, was at least worth a try :)

Link to comment
Share on other sites

Well ... you could add a fake renderer that will be controlled by the stock highlighting code in Part, then copy its material properties to all your other renderers with a strategically placed MonoBehaviour. That's the best I could come up with that will cover all the edge cases (such as when the editor logic changes highlight colors or freezes Parts). Proof of concept:

    [KSPAddon(KSPAddon.Startup.MainMenu, true)]
    class AddHighlightingFix : MonoBehaviour
    {
        public const string DummyTransformName = "dummy_highlighter_dont_delete";

        private void Start()
        {
            DontDestroyOnLoad(this);

            GetAllPartModelTransforms()
                .ToList()
                .ForEach(AddHighlightCopierAndDummyRenderer);
        }


        // note: it's important to avoid adding our MonoBehaviour directly to the Part's GO! The game will strip it out
        // on root parts if it isn't one of the allowed types (PartModule in particular but there are some others)
        private static IEnumerable<Transform> GetAllPartModelTransforms()
        {
            return PartLoader.LoadedPartsList.Where(ap => ap.partPrefab.transform.Find("model") != null)
                .Select(ap => ap.partPrefab.transform.Find("model"));
        }


        private static void AddHighlightCopierAndDummyRenderer(Transform partModelTransform)
        {
            partModelTransform.gameObject.AddComponent<CopyHighlightFromDummyRenderer>();

            var dummyContainer = new GameObject(DummyTransformName);
            dummyContainer.transform.parent = partModelTransform;
            DontDestroyOnLoad(dummyContainer);

            dummyContainer.AddComponent<MeshRenderer>();
        }
    }


    class CopyHighlightFromDummyRenderer : MonoBehaviour
    {
        private Renderer _dummyRenderer;
        private readonly MaterialPropertyBlock _propertyBlock = new MaterialPropertyBlock();

        private List<Renderer> _allOtherRenderers = new List<Renderer>();
 
        private void Awake()
        {
            var dummyTransform = transform.Find(AddHighlightingFix.DummyTransformName);

            if (!HighLogic.LoadedSceneIsEditor || dummyTransform == null || dummyTransform.renderer == null)
            {
                Destroy(this);
                return;
            }

            _dummyRenderer = dummyTransform.renderer;
        }


        private void Start()
        {
            OnTransformChildrenChanged();
        }


        private void OnTransformChildrenChanged()
        {
            _allOtherRenderers = GetComponentsInChildren<Renderer>(true).Except(new[] { _dummyRenderer }).ToList();
        }


        private void LateUpdate()
        {
            // grab current highlight colors
            _propertyBlock.SetColor(HighLogic.ShaderPropertyID_RimColor,
                _dummyRenderer.material.GetColor(HighLogic.ShaderPropertyID_RimColor));

            _propertyBlock.SetFloat(HighLogic.ShaderPropertyID_RimFalloff,
                _dummyRenderer.material.GetFloat(HighLogic.ShaderPropertyID_RimFalloff));

            // apply to all other renderers
            bool refreshChildren = false;

            foreach (var r in _allOtherRenderers)
            {
                if (r == null) // if somebody destroyed a Renderer, we'll want to refresh the list
                {
                    refreshChildren = true;
                    continue;
                }

                r.SetPropertyBlock(_propertyBlock);
            }

            if (refreshChildren) OnTransformChildrenChanged();
        }
    }

 

Link to comment
Share on other sites

5 hours ago, xEvilReeperx said:

Well ... you could add a fake renderer that will be controlled by the stock highlighting code in Part, then copy its material properties to all your other renderers with a strategically placed MonoBehaviour. That's the best I could come up with that will cover all the edge cases (such as when the editor logic changes highlight colors or freezes Parts). Proof of concept:

 

I must admit that I would not have thought about doing it that way, and I believe what you are proposing -would- work.  So... kudos, have a cookie :)

I would probably do it a bit differently though, by actually using a custom PartModule and.  That partModule would add its own GameObject w/renderer (and an empty mesh) to the model during prefab creation (this GO will be copied to the live parts by standard Unity copy/instantiation mechanics); could then easily query this (non-changing/never rem`oved) game-object for its material highlighter/color stats and re-seat them onto the actual models.  Keeps the code nice and contained within the part, and could easily be added/removed from specific parts as needed.

Probably will use this as a workaround until/if other solutions become available.

Edited by Shadowmage
Link to comment
Share on other sites

(untested) PartModule version, using the same basic methods as EvilReeper had posted above (won't be able to test it for another several hours unfortunately):

using UnityEngine;
using System.Collections.Generic;

namespace SSTUTools
{
    public class SSTUHighlightFix : PartModule
    {

        [KSPField]
        public string transformName = "HighlightingHackObject";

        private Transform dummyTransform;

        private Renderer[] cachedRenderList;

        private MaterialPropertyBlock mpb;

        private static int colorID;
        private static int falloffID;

        public override void OnStart(StartState state)
        {
            base.OnStart(state);
            MonoBehaviour.print("Starting highlighting fixer for part: " + part.name);
            dummyTransform = part.transform.FindRecursive(transformName);
            if (dummyTransform == null)//should only be null on the prefab part
            {
                GameObject newObj = new GameObject(transformName);
                newObj.transform.name = transformName;
                newObj.transform.NestToParent(part.transform.FindRecursive("model"));

                Renderer render = newObj.AddComponent<MeshRenderer>();//add a new render
                render.material = SSTUUtils.loadMaterial(null, null);//with an empty dummy material, also it doesn't actually have any mesh
                dummyTransform = newObj.transform;//cache reference to it for use for updating
            }
            if (HighLogic.LoadedSceneIsEditor)
            {
                GameEvents.onEditorShipModified.Add(new EventData<ShipConstruct>.OnEvent(onEditorVesselModified));
            }
        }

        public void Start()
        {
            colorID = HighLogic.ShaderPropertyID_RimColor;
            falloffID = HighLogic.ShaderPropertyID_RimFalloff;
            mpb = new MaterialPropertyBlock();
            updateRenderCache();
        }

        /// <summary>
        /// Event callback for when vessel is modified in the editor.  Used in this case to update the cached render list (my modular parts always call onEditorVesselModified when their models are changed, so this is a good enough catch for them)
        /// </summary>
        /// <param name="ship"></param>
        public void onEditorVesselModified(ShipConstruct ship)
        {
            if (!HighLogic.LoadedSceneIsEditor) { return; }
            updateRenderCache();
        }

        public void LateUpdate()
        {
            if (HighLogic.LoadedSceneIsEditor)
            {
                Color color = dummyTransform.renderer.material.GetColor(colorID);
                float falloff = dummyTransform.renderer.material.GetFloat(falloffID);

                mpb.SetColor(colorID, color);
                mpb.SetFloat(falloffID, falloff);
                bool updateCache = false;
                int len = cachedRenderList.Length;
                for (int i = 0; i < len; i++)
                {
                    //somehow we got a nulled out render, object was likely deleted -- update the cached list, will be correct for the next tick/update cycle
                    if (cachedRenderList[i] == null)
                    {
                        updateCache = true;
                        continue;
                    }
                    cachedRenderList[i].SetPropertyBlock(mpb);
                }
                if (updateCache)
                {
                    updateRenderCache();
                }
            }
        }

        private void updateRenderCache()
        {
            cachedRenderList = null;
            Renderer[] renders = part.transform.GetComponentsInChildren<Renderer>(true);
            List<Renderer> rendersToCache = new List<Renderer>();
            int len = renders.Length;
            for (int i = 0; i < len; i++)
            {
                //skip the dummy renderer; though it honestly should not matter if it is in the list or not, as we are pulling the current vals from it before setting anything
                if (renders[i].transform != dummyTransform)
                {
                    rendersToCache.Add(renders[i]);
                }
            }
            cachedRenderList = rendersToCache.ToArray();
        }

    }
}

Might just merge this code into the existing effected PartModules... or might leave it as a stand-alone fix (as that is much less copy/pasting/editing of existing code).

Will update this post if I find the above code needs changed/edited/fixed/updated.

 

And thanks for the help and ideas on how to solve/work around this one -- have been banging my head on it for literally months with no other progress :)

 

Edit: Updated with fixed/tested/working code

Edited by Shadowmage
Link to comment
Share on other sites

This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...