Jump to content

Asset Bundles - Use for Part Models - Info and Questions - Enable use of BlendShapes animations


Recommended Posts

The problems:

BlendShapes are unsupported in .mu files / PartTools, but will load in an AssetBundle.
AssetBundles do not by default load models in a way that is accessible to use in a part through a MODEL node.

 

The solution (so far):

Proper setup of the AssetBundle in Unity, custom AssetBundle extension, and some custom plugin code to load the model into the GameDatabase.

 

Useful Information Summary:

  • This method uses custom asset bundle naming and composition.  Only a single .prefab GameObject model should reside in each bundle (currently; investigating multi-model-loading capabilities).  Textures should reside in a standard folder beside the asset bundle; in essence you can treat the AssetBundle-Model as if it were a .mu model file.
  • AssetBundles need a custom extension to trigger loading by the DatabaseLoader code.  This can be whatever you want to use, but SHOULD NOT BE be the .ksp extension (as that is used by asset bundles that the game will load). See the code examples below.
  • Once successfully loaded the models can be used in a part just as any other model can.  The URL for the model will be the path and name of the asset-bundle, minus extension.
  • See below for information regarding BlendShapes; basically though they 'just work'.

 

BlendShape Animation Info:

  • Models loaded through AssetBundles have full support for SkinnedMeshRenderer and BlendShape based animations. 
    • Create the animation in Unity with keyframes for the BlendShape key weights, and reference that animation through a ModuleAnimateGeneric or any other animation handling module.
    • See the information above and the code below for details on how to get the models added to parts

 

Code Examples:

DatabaseLoader based loading code (inspired/derived from @xEvilReeperx example below); mildly WIP, but should work for most uses.

using System;
using System.Collections.Generic;
using System.Linq;

using UnityEngine;
using System.Collections;
using System.IO;

namespace SSTUTools
{
    [DatabaseLoaderAttrib((new string[] { "smf" }))]
    public class SMFBundleDefinitionReader : DatabaseLoader<GameObject>
    {
        public override IEnumerator Load(UrlDir.UrlFile urlFile, FileInfo file)
        {
            // KSP-PartTools built AssetBunldes are in the Web format, 
            // and must be loaded using a WWW reference; you cannot use the 
            // AssetBundle.CreateFromFile/LoadFromFile methods unless you 
            // manually compiled your bundles for stand-alone use
            WWW www = CreateWWW(urlFile.fullPath);
            //not sure why the yield statement here, have not investigated removing it.
            yield return www;

            if (!string.IsNullOrEmpty(www.error))
            {
                MonoBehaviour.print("Error while loading AssetBundle model: " + www.error);
                yield break;
            }
            else if (www.assetBundle == null)
            {
                MonoBehaviour.print("Could not load AssetBundle from WWW - " + www);
                yield break;
            }

            AssetBundle bundle = www.assetBundle;
                        
            //TODO clean up linq
            string modelName = bundle.GetAllAssetNames().FirstOrDefault(assetName => assetName.EndsWith("prefab"));
            AssetBundleRequest abr = bundle.LoadAssetAsync<GameObject>(modelName);
            while (!abr.isDone) { yield return abr; }//continue to yield until the asset load has returned from the loading thread
            if (abr.asset == null)//if abr.isDone==true and asset is null, there was a major error somewhere, likely file-system related
            {
                MonoBehaviour.print("ERROR: Failed to load model from asset bundle!");
                yield break;
            }
            GameObject model = GameObject.Instantiate((GameObject)abr.asset);//make a copy of the asset
            setupModelTextures(urlFile.root, model);
            this.obj = model;
            this.successful = true;
            //this unloads the compressed assets inside the bundle, but leaves any instantiated models in-place
            bundle.Unload(false);
        }

        /// <summary>
        /// Creates a WWW URL reference for the input file-path
        /// </summary>
        /// <param name="bundlePath"></param>
        /// <returns></returns>
        private static WWW CreateWWW(string bundlePath)
        {
            try
            {
                string name = Application.platform == RuntimePlatform.WindowsPlayer ? "file:///" + bundlePath : "file://" + bundlePath;
                return new WWW(Uri.EscapeUriString(name));
            }
            catch (Exception e)
            {
                MonoBehaviour.print("Error while creating AssetBundle request: " + e);
                return null;
            }
        }

        private static void setupModelTextures(UrlDir dir, GameObject model)
        {
            Renderer[] renders = model.GetComponentsInChildren<Renderer>(true);
            Material m;
            List<Material> adjustedMaterials = new List<Material>();
            foreach (Renderer render in renders)
            {
                m = render.sharedMaterial;
                if (adjustedMaterials.Contains(m)) { continue; }//already fixed that material (many are shared across transforms), so skip it
                else { adjustedMaterials.Add(m); }                
                replaceTexture(m, "_MainTex", false);
                replaceTexture(m, "_BumpMap", true);
                replaceTexture(m, "_Emissive", false);
            }
        }

        private static void replaceTexture(Material m, string name, bool nrm = false)
        {
            Texture tex = m.GetTexture(name);
            if (tex != null && !string.IsNullOrEmpty(tex.name))
            {
                Texture newTex = findTexture(tex.name, nrm);
                if (newTex != null)
                {
                    m.SetTexture(name, newTex);
                }
            }
        }

        private static Texture2D findTexture(string name, bool nrm = false)
        {
            //TODO clean up foreach
            foreach (GameDatabase.TextureInfo t in GameDatabase.Instance.databaseTexture)
            {
                if (t.file.url.EndsWith(name))
                {
                    if (nrm) { return t.normalMap; }
                    return t.texture;
                }
            }
            return null;
        }
        
    }

}


Example part-config (abridged):

Example no longer needed; use the model just like you would any other model file though a MODEL node. 

 

 

Thanks in advance for any information offered, and hopefully others' can make some use out of what I have discovered so far.

Edited by Shadowmage
Link to comment
Share on other sites

Have you considered writing a DatabaseLoader instead of using a PartModule? Here's a version of mine I tweaked to handle dependencies like you want that can replace the Mu exporter. Create an AssetBundle with exactly one GameObject prefab in it, rename the extension to "ru" and then treat it exactly like you would a .mu. It supports loading dependencies from KSPs and other RUs. It's still rough proof of concept code though so there are probably some bugs

Spoiler

[DatabaseLoaderAttrib((new[] {"ru"}))]
public class RuPartBundleDefinitionReader : DatabaseLoader<GameObject>
{
    private const string ManifestSuffix = "_bundle.xml";

    public override IEnumerator Load(UrlDir.UrlFile urlFile, FileInfo file)
    {
        Debug.Log("Load (RU): " + urlFile.url);

        // load the AssetBundle
        var www = CreateWWW(urlFile.fullPath);

        yield return www;

        if (!string.IsNullOrEmpty(www.error))
        {
            Debug.LogError("Error while loading AssetBundle: " + www.error);
            yield break;
        } else if (www.assetBundle == null)
        {
            Debug.LogError("Could not load AssetBundle");
            yield break;
        }

        var bundle = www.assetBundle;

        AssetLoader.LoadedBundles.Add(bundle); // just in case something goes wrong in the next few lines

        // Grab its manifest
        var manifest = bundle.GetAllAssetNames().FirstOrDefault(assetName => assetName.EndsWith(ManifestSuffix));

        if (string.IsNullOrEmpty(manifest))
        {
            Debug.LogWarning("No manifest found for this bundle.");
            yield break;
        }

        var request = bundle.LoadAssetAsync<TextAsset>(manifest);

        yield return request;

        if (request.asset == null)
        {
            Debug.LogError("Failed to load manifest: " + manifest);
            yield break;
        }

        try
        {
            var xmlManifest = (TextAsset) request.asset;
            var bundleDefinition = LoadBundleDefinition(xmlManifest);

            bundleDefinition.path = urlFile.fullPath;

            // not added to BundleDefinitions/AssetDefinitions because the AssetLoader will wipe those lists shortly

            AssetBundlePartLoadingSystem.AddTarget(bundleDefinition, urlFile);
        }
        finally
        {
            AssetLoader.LoadedBundles.Remove(bundle);
            bundle.Unload(true);
        }
    }

    private static BundleDefinition LoadBundleDefinition([NotNull] TextAsset asset)
    {
        if (asset == null) throw new ArgumentNullException("asset");

        var bundleDefinition = BundleDefinition.CreateFromText(asset.text);

        bundleDefinition.assets.ForEach(ad => ad.bundle = bundleDefinition);

        return bundleDefinition;
    }



    private static WWW CreateWWW(string bundlePath)
    {
        try
        {
            return 
                new WWW(
                    Uri.EscapeUriString(Application.platform == RuntimePlatform.WindowsPlayer
                        ? "file:///" + bundlePath
                        : "file://" + bundlePath));
        }
        catch (Exception e)
        {
            Debug.LogError("Error while creating AssetBundle request: " + e);
            return null;
        }
    }
}
  
[KSPAddon(KSPAddon.Startup.Instantly, true)]
public class AssetBundlePartLoadingSystem : LoadingSystem
{
    struct BundleInfo
    {
        public BundleDefinition Definition { get; private set; }
        public UrlDir.UrlFile File { get; private set; }

        public BundleInfo(BundleDefinition definition, UrlDir.UrlFile file) : this()
        {
            if (definition == null) throw new ArgumentNullException("definition");
            if (file == null) throw new ArgumentNullException("file");

            Definition = definition;
            File = file;
        }
    }

    struct BundleComparer : IComparer<BundleInfo>
    {
        public int Compare(BundleInfo x, BundleInfo y)
        {
            if (x.Definition.dependencyBundles.Contains(y.Definition)) return 1;
            if (y.Definition.dependencyBundles.Contains(x.Definition)) return -1;
            return 0;
        }
    }

    private static readonly List<BundleInfo> Targets = new List<BundleInfo>();

    private int _currentTarget = 0;
    private string _progressTitle;
    private bool _busy = false;

    public static void AddTarget(BundleDefinition definition, UrlDir.UrlFile file)
    {
        Targets.Add(new BundleInfo(definition, file));
    }


    private void Awake()
    {
        var loaders = LoadingScreen.Instance.loaders;
        loaders.Insert(loaders.IndexOf(GameDatabase.Instance) + 1, this); // after GameDatabase
    }

    public override bool IsReady()
    {
        return Targets.Count == 0;
    }

    public override void StartLoad()
    {
        base.StartLoad();

        // GameDatabase:AssetLoader will have wiped these lists, add them back

        AssetLoader.BundleDefinitions.AddRange(Targets.Select(t => t.Definition));
        AssetLoader.AssetDefinitions.AddRange(Targets.SelectMany(t => t.Definition.assets));

 
        MatchDependencyNamesToDefinitions();

        foreach (var dependenciesNotMet in Targets.Where(t => !t.Definition.dependenciesFound))
            Debug.LogWarning("The following dependency/dependencies for " + dependenciesNotMet.File.url +
                                " are missing: " +
                                string.Join(",", dependenciesNotMet.Definition.dependencyNames.ToArray()));

        Targets.RemoveAll(t => !t.Definition.dependenciesFound);
        Targets.Sort(new BundleComparer());

#if DEBUG
        foreach (var t in Targets)
        {
            Log.Warning("Will load: " + t.Definition.name);

            foreach (var d in t.Definition.dependencyBundles)
                Log.Warning("  has dependency: " + d.urlName + ", " + d.path);
        }
#endif

        StartCoroutine(BeginLoading());

    }

    public override float ProgressFraction()
    {
        return Targets.Count == 0 ? 1f : _currentTarget / (float) Targets.Count;
    }

    public override string ProgressTitle()
    {
        return _progressTitle;
    }


    private static void MatchDependencyNamesToDefinitions()
    {
        foreach (var target in Targets)
            target.Definition.dependenciesFound = CollectDependencies(target.Definition, out target.Definition.dependencyBundles);
    }



    private static bool CollectDependencies(BundleDefinition definition, out List<BundleDefinition> dependencyDefinitions)
    {
        dependencyDefinitions = new List<BundleDefinition>();

        foreach (var dependencyName in definition.dependencyNames)
        {
            var dependencyDefinition =
                AssetLoader.LoadedBundleDefinitions.FirstOrDefault(bd => bd.urlName == dependencyName) ??
                AssetLoader.BundleDefinitions.FirstOrDefault(bd => bd.urlName == dependencyName);

            if (dependencyDefinition != null)
                dependencyDefinitions.Add(dependencyDefinition);
        }

        return dependencyDefinitions.Count == definition.dependencyNames.Count;
    }


    private IEnumerator BeginLoading()
    {
        _currentTarget = 0;

        for (; _currentTarget < Targets.Count; ++_currentTarget)
        {
            try
            {
                var current = Targets[_currentTarget];
                AssetDefinition prefabDefinition = null;

                if (!GetPrefabAssetDefinition(current.Definition, out prefabDefinition))
                    continue;

                var title = "Loading RU: " + Targets[_currentTarget].File.url;
                _progressTitle = title;
                Debug.Log(title);

                _busy = true;

                AssetLoader.LoadAssets(l =>
                {
                    AddModel(l, current.File);
                }, prefabDefinition);
            }
            catch (Exception e)
            {
                Debug.LogError("Failed to load target due to: " + e);
                _busy = false;
            }

            while (_busy) yield return null;
            
        }

        Targets.Clear();
    }


    private void AddModel(AssetLoader.Loader loader, UrlDir.UrlFile file)
    {
        _busy = false;

        try
        {
            var prefab = (GameObject) loader.objects.Single();

            prefab.name = file.url;
            GameDatabase.Instance.databaseModel.Add(prefab);
            GameDatabase.Instance.databaseModelFiles.Add(file);
        }
        catch (Exception e)
        {
            Debug.LogError("Failed to load RuPart");
        }
    }


    private static bool GetPrefabAssetDefinition(BundleDefinition definition, out AssetDefinition prefabDefinition)
    {
        var assets = definition.GetAssetsWithType(typeof (GameObject));

        if (assets.Length != 1)
        {
            prefabDefinition = null;
            return false;
        }

        prefabDefinition = assets.First();
        return true;
    }
}

 

 

 

Link to comment
Share on other sites

8 hours ago, xEvilReeperx said:

Have you considered writing a DatabaseLoader instead of using a PartModule? Here's a version of mine I tweaked to handle dependencies like you want that can replace the Mu exporter. Create an AssetBundle with exactly one GameObject prefab in it, rename the extension to "ru" and then treat it exactly like you would a .mu. It supports loading dependencies from KSPs and other RUs. It's still rough proof of concept code though so there are probably some bugs

 

 

Very interesting; I was not aware such capability existed.  Ohh what I would give for for full source access and a properly documented API.  Thanks for the detailed code example, should go a long ways towards helping me get this all sorted out.

Will do some investigation on this tonight; seems like it will end up being a more compatible way to load those models up.

 

I'm still curious though as to how the textures for models loaded through AssetBundles are supposed to work/going to work; during my investigations last night I could not get the textures to load up properly when included in the bundle with the model, nor could I find how to use existing textures from other sources.  The materials appeared to load up properly (appeared to, but could not verify due to texture problems), but the textures were simply MIA.

With this database-loader driven method, can the textures simply reside in the same folder as the .ru model (asset-bundle) file (as would be done with a .mu)?


Anyhow, thanks again for the information, will update later this evening after I have time to play around with it a bit.

Link to comment
Share on other sites

39 minutes ago, Shadowmage said:

With this database-loader driven method, can the textures simply reside in the same folder as the .ru model (asset-bundle) file (as would be done with a .mu)?

The textures will be embedded inside the bundle itself (unless they've been specifically assigned to another bundle: .ru or .ksp will work). You could definitely have it override whatever texture was in the bundle though

Link to comment
Share on other sites

On Friday, July 15, 2016 at 2:33 AM, xEvilReeperx said:

Have you considered writing a DatabaseLoader instead of using a PartModule?

 

Thanks again for the hint towards and examples on the DatabaseLoader method; it did turn out to be a much cleaner implementation, and allowed me to load the models to be accessed just like any other while using textures just as any other model would (e.g. textures are not in any bundle and reside in the folder with the model).

I had to do manual re-assignment of the textures as they were not part of the bundle that I exported, but the texture names were present in the Material for the model, so was easy enough to recover them / find them and slot them into the material.  This allows for re-use of existing textures from other models / shared textures / anything else that could be done with a .mu model.

I'll post up my bit of code after I do some cleanup and further testing.  It is certainly working well with this one simple test case, but there is always a chance for bugs/issues as it gets used for more complex setups.

Edited by Shadowmage
Link to comment
Share on other sites

  • 2 weeks later...
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...