Jump to content

[SOLVED] Property animations with custom shaders


Recommended Posts

I've found that making a shader, then creating a ksp asset bundle and then copying it somewhere into GameData won't work,  mu. model using that shader still won't load.

I've looked around for sources on loading custom shaders and i can put something together.

What i'm not sure is if a .mu file contains an animation that drives a property inside this custom shader the part will still fail to load if shader is not beforehand loaded/registered into KSP.

If the above is true, do i need to write a plugin that loads BEFORE parts start to get loaded ? Like ModuleManager does for example.

@Shadowmage Please enlighten me on this. Thanks in advance.

 

Edited by fatcargo
Link to comment
Share on other sites

15 hours ago, fatcargo said:

What i'm not sure is if a .mu file contains an animation that drives a property inside this custom shader the part will still fail to load if shader is not beforehand loaded/registered into KSP.

I'm not sure on that; good question.

15 hours ago, fatcargo said:

If the above is true, do i need to write a plugin that loads BEFORE parts start to get loaded ? Like ModuleManager does for example.

Good idea either way; you would always want your shader loaded before the parts are loaded.  That is what TU does, and has no issues with applying shaders to parts/etc.  And yes, you -will- need custom code to apply the shader to your parts;  KSP's PartTools/PartLoader absolutely will not apply non-KSP shaders.

In fact, if you are interested, TU can likely handle all of your shader loading and applying of shaders to models (it is what it is meant to do, after-all).  If you would like more information/further instructions on that, please let me know and I'll work something up.

 

Now, I'm not sure on animating of shader properties for non-KSP shaders; I've never tried it.  It would depend on a lot of internals that I don't have access to -- does KSP PartTools export/encode the animation keyed on a property name, or some internal property id number?  Does Unity itself use a property name (string), or an id number when manipulating the animation at runtime?  If they both use property names, likely it will all work fine.  If they utilize some other method... I'm not sure how it would be made to work.  (I've seen issues with Unity ID numbers before, in regards to layer/tag numbers and how they are generated at compile time by the editor, not matching up with id #'s for items added at runtime)

Link to comment
Share on other sites

NB: i forgot to say that i want to scroll the mesh over texture's UV space, for a scrolling effect over surface. Maybe all this is not needed to accomplish what i want.

The property animation is a text file and uses "attribute" and "path" human-readable strings to specify targeted property. It could be specific to mesh if "path" is directly referencing the mesh in model.

I've tried making my own shader loader but it failed (as expected).  It was tagged as KSPAddon with instant startup. It appears to have successfully loaded the .ksp bundle, found the shader and loaded it into KSP, but the part using it still failed to load. Debug dump shows PartReader.ReadAnimation, so i'm sure it fails at animation.

I've searched for info on custom shaders and seen that mostly they're loaded first, then applied to parts. Which is ok, but i have an animation referencing the shader, and i'd really like not to load that as well separately at runtime, and then apply custom shader. Looks like a lot of crutches propping up KSP API.

IF TU can really load shaders before parts with property animations start being loaded and parsed, then i'd like to know more.

Note that i did manage to create animation to drive shader property in unity editor. Below are contents of two files used to demostrate this.

Here are two text blocks, save them in Unity project for testing in editor.

For file "CUSTOMScrollingUnlit.shader" i keep in "Assets\Shaders" folder

Shader "KSP/FX/CUSTOMScrollingUnlit"
{
    Properties
    {
        _MainTex("MainTex (RGB Alpha(A))", 2D) = "white" {}
        _Color("Color (RGB Alpha(A))", Color) = (1, 1, 1, 1)
        [Space]
        _CoordinateU("_CoordinateU", Range(0, 1)) = 0
        _CoordinateV("_CoordinateV", Range(0, 1)) = 1
        [PerRendererData]_UnderwaterFogFactor("Underwater Fog Factor", Range(0, 1)) = 0
    }
    
    SubShader
    {
        Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }

        Pass
        {
            ZWrite On
            ColorMask 0
        }

        ZWrite On
        ZTest LEqual
        Blend SrcAlpha OneMinusSrcAlpha

        CGPROGRAM

        #include "../PartTools/LightingKSP.cginc"
        
        #pragma surface surf Unlit noforwardadd noshadow noambient novertexlights alpha:fade
        #pragma target 3.0

        sampler2D _MainTex;

        float _CoordinateU;
        float _CoordinateV;

        struct Input
        {
            float2 uv_MainTex;
            float3 worldPos;
        };

        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed2 scrollUV = IN.uv_MainTex ;            
            fixed xScrollValue = _CoordinateU;
            fixed yScrollValue = _CoordinateV;
            scrollUV += fixed2(xScrollValue, yScrollValue);
            half4 c = tex2D(_MainTex, scrollUV);
            float3 normal = float3(0, 0, 1);

            float4 fog = UnderwaterFog(IN.worldPos, c.rgb * _Color.rgb);

            o.Albedo = fog.rgb;
            o.Normal = normal;
            o.Alpha = c.a * _Color.a * fog.a;
        }
        ENDCG
    }
}

 

For file "test.anim" i keep in "Assets" folder. It drives "_CoordinateU" and "_CoordinateV" properties to move texture across a surface.

 

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!74 &7400000
AnimationClip:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_Name: test
  serializedVersion: 6
  m_Legacy: 1
  m_Compressed: 0
  m_UseHighQualityCurve: 1
  m_RotationCurves: []
  m_CompressedRotationCurves: []
  m_EulerCurves: []
  m_PositionCurves: []
  m_ScaleCurves: []
  m_FloatCurves:
  - curve:
      serializedVersion: 2
      m_Curve:
      - serializedVersion: 3
        time: 0
        value: 0
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      - serializedVersion: 3
        time: 1
        value: 1
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      m_PreInfinity: 2
      m_PostInfinity: 2
      m_RotationOrder: 4
    attribute: material._CoordinateU
    path:
    classID: 23
    script: {fileID: 0}
  - curve:
      serializedVersion: 2
      m_Curve:
      - serializedVersion: 3
        time: 0
        value: 0.9
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      - serializedVersion: 3
        time: 1
        value: 0.2
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      m_PreInfinity: 2
      m_PostInfinity: 2
      m_RotationOrder: 4
    attribute: material._CoordinateV
    path:
    classID: 23
    script: {fileID: 0}
  m_PPtrCurves: []
  m_SampleRate: 60
  m_WrapMode: 0
  m_Bounds:
    m_Center: {x: 0, y: 0, z: 0}
    m_Extent: {x: 0, y: 0, z: 0}
  m_ClipBindingConstant:
    genericBindings: []
    pptrCurveMapping: []
  m_AnimationClipSettings:
    serializedVersion: 2
    m_AdditiveReferencePoseClip: {fileID: 0}
    m_AdditiveReferencePoseTime: 0
    m_StartTime: 0
    m_StopTime: 1
    m_OrientationOffsetY: 0
    m_Level: 0
    m_CycleOffset: 0
    m_HasAdditiveReferencePose: 0
    m_LoopTime: 1
    m_LoopBlend: 0
    m_LoopBlendOrientation: 0
    m_LoopBlendPositionY: 0
    m_LoopBlendPositionXZ: 0
    m_KeepOriginalOrientation: 0
    m_KeepOriginalPositionY: 1
    m_KeepOriginalPositionXZ: 0
    m_HeightFromFeet: 0
    m_Mirror: 0
  m_EditorCurves:
  - curve:
      serializedVersion: 2
      m_Curve:
      - serializedVersion: 3
        time: 0
        value: 0
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      - serializedVersion: 3
        time: 1
        value: 1
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      m_PreInfinity: 2
      m_PostInfinity: 2
      m_RotationOrder: 4
    attribute: material._CoordinateU
    path:
    classID: 23
    script: {fileID: 0}
  - curve:
      serializedVersion: 2
      m_Curve:
      - serializedVersion: 3
        time: 0
        value: 0.9
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      - serializedVersion: 3
        time: 1
        value: 0.2
        inSlope: 0
        outSlope: 0
        tangentMode: 136
        weightedMode: 0
        inWeight: 0.33333334
        outWeight: 0.33333334
      m_PreInfinity: 2
      m_PostInfinity: 2
      m_RotationOrder: 4
    attribute: material._CoordinateV
    path:
    classID: 23
    script: {fileID: 0}
  m_EulerEditorCurves: []
  m_HasGenericRootTransform: 0
  m_HasMotionFloatCurves: 0
  m_Events: [] 

I added "test.anim" to list of animations in Animation component, which is located inside mesh object whose texture i want to animate.

Edited by fatcargo
Link to comment
Share on other sites

I haven't been up with the latest stuff so I'm not sure if it's still functional, but the limitations of the mu loader are exactly why I loaded certain assets as asset bundles instead. @Shadowmage has a version posted, in fact:

If you make sure your textures are included in the bundle, you shouldn't even need any post-loading stuff

Link to comment
Share on other sites

2 hours ago, fatcargo said:

NB: i forgot to say that i want to scroll the mesh over texture's UV space, for a scrolling effect over surface.

Ahh, yes.  Certainly a viable effect, one that I've used on occasion (in Kerbal Foundries, on the track parts); though for my uses, I animate the properties directly through plugin rather than a pre-compiled animation.  But a precompiled animation should also work.

2 hours ago, fatcargo said:

Debug dump shows PartReader.ReadAnimation, so i'm sure it fails at animation.

It sounds like the problem you are running into is in fact due to the animation itself, and not necessarily the shader.  Likely that those properties are simply not supported by the PartTools system.  What @xEvilReeperx proposed is indeed the solution -- import your entire model through an AssetBundle, where you can use any kind of animation that is supported in the Unity Editor.  Of course, it is yet-more-steps in what should be a simple process, but such is the nature of workarounds.  (TU can _also_ load asset-bundle based models if needed).


So, in short:  to accomplish UV animation on a non-KSP shader you will need custom loading from asset-bundles.  First a custom loader for the shader itself, and then a custom loader for the models (that may or may not need to restore texture references depending on how everything else was structured/compiled).

If the above is not enough information, please let me know and I can dig into more specifics.  Sounds like what you are desiring to do should be entirely doable, might just be a bit of work getting there.

Link to comment
Share on other sites

In another attempt i removed completely animation from model with custom shader and it failed to load again. This time error it clearly showed in debug that unknown shader was used.

Relevant lines from debug log:
 

File error:
Value cannot be null.
Parameter name: shader
  at (wrapper managed-to-native) UnityEngine.Material.CreateWithShader(UnityEngine.Material,UnityEngine.Shader)
  at UnityEngine.Material..ctor (UnityEngine.Shader shader) [0x00009] in C:\buildslave\unity\build\Runtime\Export\Shaders\Shader.bindings.cs:118

 

If i pursue this any further it may be needed to apply both shaders and animations at runtime. For the time being, this thread may be considered to be on pause.

Link to comment
Share on other sites

Ok thread restart.

I was trying to analyze where i made a mistake but had no luck.

@xEvilReeperx question about building assets : is the following link ...

... still a valid method ?

I did found one interesting thing : "GameData\Squad\squadcorefx.ksp" is most likely place where shaders are stored, and models have no problem using those assets at loading time. Why is same not true for custom made .ksp assets ?

Edited by fatcargo
Link to comment
Share on other sites

That method will work (for your shader problem). What exactly is the issue? The Shader.Find API apparently requires that the shaders exist at project build time, which is why they aren't found when the PartLoader is creating materials. That's why I suggest you bypass PartLoader entirely. If you put your shader AND model into an asset bundle and then use the previously posted code to load that as though it was a model, you won't be restricted to the extremely limited stuff PartLoader does.

The shaders are all in squadcore.ksp. squadcorefx contains TextMeshPro shaders. You can inspect these using the Asset Bundle Browser, a free Unity asset you can download from their store

Link to comment
Share on other sites

Thanks for the info. I will use this for models with complex animations.

Though one thing still bugs me : what to do with cfg file ? It too needs to go into asset ? If i leave it in GameData, won't PartLoader try to load .mu model (which now resides in asset that i'll have to load later when my shader can be added to KSP), and then fail ? And if i have to load .cfg from asset, then MM won't be able to access it ?

Does KerbalKonstructs do something like this ? I  looked inside sources for KK and made a small shader loader plugin that was supposed to load shaders prior to PartLoader start and it failed to do anything. To paraphrase linked forum post - the earliest time to load a shader is at MonoBehaviour.Awake() when LoadedScene is MainMenu.

What also bugs me how come PartLoader doesn't fail on models containing KSP shaders ? If original KSP shaders are loaded later on, original models should also fail ?

Edited by fatcargo
Link to comment
Share on other sites

You don't put the .mu model into the bundle. You put the actual GameObject that you've set up (as a prefab) in there instead, bypassing the .mu creation step entirely. This asset bundle is essentially a mu file now, except it uses Unity's serialization instead of Squad's hand-rolled stuff that is missing lots of features. Everything else behaves exactly the same as if you were using a mu file -- define your cfg in exactly the same way. By the time the PartLoader is creating parts out of the loaded models, all of that stuff has been run and shaders are in-place.

Link to comment
Share on other sites

Sorry to bother you again but i've failed after several variants.

This is what i did as a starting point :

1. create empty GameObject, rename to "test"

2. create simple cube named "Cube"

3. drag cube in game object as child

2. create material (resides in "Materials" folder)

3. assign "KSP/Diffuse" shader to material

4. assign material to mesh renderer component of cube object, add PNG texture

5. create "Prefabs" folder in asset explorer

6. drag and drop "cube" object from inspector to "Prefabs" folder to create a prefab

7. select prefab in asset explorer and select AssetBundle as "new" and type "cube"

8. select from menu KSPAssets > Asset Compiler

9. click on "Build" in "cube" row (make sure "auto" is selected)

10. wait for build to complete and then copy "cube.ksp" into folder "ATestCube" inside "GameData" (size is about 0.5 MB, mostly because of texture)

11. in same "ATestCube" folder create "cube.cfg" text file with
 

PART

{

//...

mesh = Cube

//...

}

12. run KSP and it shows error,  says "PartCompiler: Cannot clone model from 'ATestCube' directory as model does not exist", though log file says that "cube.ksp" asset is loaded

I also tried with MODEL{} to try and target my model by path

PART
{
  // ...
  MODEL
  {
    model = Prefabs/test
    position = 0.0, 0.0, 0.0
  }
  // ...
}

But it too failed with message "PartCompiler: Cannot clone model 'Prefabs/test' as model does not exist"

So, how do i now reference my model in part config ?

 

Edited by fatcargo
Link to comment
Share on other sites

Did you use the code from Shadowmage's post? In that version, the file extension should be 'smf' (although you should probably choose your own extension and edit the code accordingly). You also don't need to use the KSP asset compiler, although it shouldn't be a problem if you do

Link to comment
Share on other sites

Ohh i've thought that putting the asset file inside GameData is enough so that KSP's loader would take care of the rest. I guess custom loader is still needed.

Ok i'll review the post(s) and build this. Its late i'll try again tomorrow.

Link to comment
Share on other sites

Ok i think i've analyzed enough of the code to continue discussion.

First off, i've got Textures Unlimited 1.5.8.23 from ShadowMage's github repo. Secondly, if there is a more appropriate thread to discuss this (so as to not unnecessarily divide/divert attention) do please point me to it.

i've noticed that central part is in TexturesUnlimited-1.5.8.23\Plugin\SSTUTools\KSPShaderTools\Util\SMFBundleDefinitionReader.cs which contains attribute

[DatabaseLoaderAttrib((new string[] { "smf" }))]

Is "smf" an extension that will trigger this plugin to load it ? I'm not familiar with this attribute.

Next, further in source there is local var

List<Material> adjustedMaterials = new List<Material>();

which appears not to be used for anything. Materials get added to it and nothing else. And being local it is not usable by anything outside of setupModelTextures(). Maybe some kind of cache used in future ?

Why is this asset loading method limited to single model only ? I ask because loading of multiple models from single assets file may have savings in reusing textures and shaders as well. I admit i don't have much expirience with assets beyond some (failed) experiments. Will look into this more.

One more thing : in above mentioned forum discussion about assets and models you have used asset's XML file (ending in " _bundle.xml") which i think should be handled in TU plugin as it provides all resources listed by name and type (i personally like this and will use it when i try again to make my loader plugin). What does worry me is if i try to pack multiple files with same name (for example multiple models that use different textures in different directories but otherwise have same filename).

And last : is there an example asset file that features a model, shader and texture ? I'd like to have a look at it.

Link to comment
Share on other sites

The game scans for all DatabaseLoaderAttrib-decorated types and uses them to load files from GameData directory. You can use this to have the game do stuff with your own files. I recommend you choose your own extension (it can be whatever you like that isn't already taken).

It looks like ShadowMage needed to manipulate the materials, and uses that list to keep track of materials that have already been adjusted. My prototype didn't need to do this because I stored everything (textures and materials) inside the asset bundles themselves, so it all just works.

RE your concern of multiple-named files: an advantage of asset bundles is that they can share this information already. That's why the XML definitions might be useful to you. You could put all the shared data into its own bundle, and as long as this bundle is loaded, textures and other assets will be shared.

There isn't any technical reason you couldn't load lots of models or whatever else at a time from the bundle. That would definitely be the simplest, most straight-forward way to handle shared dependencies though: throw all your stuff into one, single file.

Link to comment
Share on other sites

On 1/7/2020 at 4:44 PM, fatcargo said:

And last : is there an example asset file that features a model, shader and texture ? I'd like to have a look at it.

I've put together a bare-bones example that does what you want. Code:

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using KSPAssets;

namespace CargoLoader
{
    [DatabaseLoaderAttrib(new[] { "cargo" })]
    public class CargoLoader : DatabaseLoader<GameObject>
    {
        private readonly List<AssetBundle> _loadedBundles = new List<AssetBundle>();

        private void Failed(string reason)
        {
            successful = false;
            Debug.LogError(reason);
        }


        public override IEnumerator Load(UrlDir.UrlFile urlFile, FileInfo file)
        {
            // load the AssetBundle
            var bundleRequest = AssetBundle.LoadFromFileAsync(file.FullName);
            yield return bundleRequest;

            var bundle = bundleRequest.assetBundle;

            if (bundle == null)
            {
                Failed("Unable to load: " + urlFile.url);
                yield break;
            }

            _loadedBundles.Add(bundle);

            // use manifest to determine URLs of models
            //
            // -> first we need to find it. It's probably named the same as the
            // bundle, but let's not rely on that in case it gets renamed
            var manifest = bundle.GetAllAssetNames().FirstOrDefault(
                assetName => assetName.EndsWith("_bundle.xml"));

            if (string.IsNullOrEmpty(manifest))
            {
                Failed("Could not find manifest inside " + urlFile.url);

                foreach (var name in bundle.GetAllAssetNames())
                {
                    Debug.Log("  name: " + name);
                }

                yield break;
            }

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

            yield return manifestRequest;

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

            var xmlManifest = (TextAsset)manifestRequest.asset;
            var bundleDefinition = BundleDefinition.CreateFromText(xmlManifest.text);

            bundleDefinition.path = urlFile.fullPath;

            foreach (var def in bundleDefinition.GetAssetsWithType(typeof(GameObject)))
            {
                var assetRequest = bundle.LoadAssetAsync<GameObject>(def.name);
                yield return assetRequest;

                var asset = assetRequest.asset as GameObject;

                if (asset == null)
                {
                    Debug.LogWarning(string.Format("Failed to load asset '{0}' from {1}", def.name, urlFile.url));
                    // don't quit here: maybe there are multiple assets in this bundle
                }

                // two options here: use the Unity path, or just take the name and generate
                // a URL using the path of the bundle. Latter option makes more sense to me
                // note: this means the name of the bundle is used within the part's url
                // example: GameData/myfolder/mybundle.cargo has two parts, part1 and part2, inside it
                // you would reference these parts in a config as myfolder/mybundle/part1

                var name = Path.GetFileNameWithoutExtension(def.name);
                var url = urlFile.parent.url + "/" + urlFile.name + "/" + name;

                asset.name = url;
                asset.transform.parent = GameDatabase.Instance.transform;

                GameDatabase.Instance.databaseModel.Add(asset);

                // this next line might be a problem if you have multiple models in one bundle
                // unsure when and where it's used
                GameDatabase.Instance.databaseModelFiles.Add(urlFile);

                Debug.Log("Loaded " + url + " from " + urlFile.url);
            }
        }

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

            foreach (var bundle in _loadedBundles)
                bundle.Unload(false);
        }
    }
}

This will load all prefab GameObjects from a KSP bundle created with PartTools (renamed with .cargo extension) and make them available for use from MODEL nodes inside part configs. It does nothing extra or fancy (no dependency resolution) but just cramming everything into one bundle is convenient and ensures resources are shared.

The bundle I've provided contains two prefabs named Cube and Sphere. Cube uses a custom shader I slapped together. Sphere uses one of the shaders from PartTools. Both have a shader property driven by an animation. You refer to these in the same way that you would refer to a mu file, with the exception that the bundle is part of the path. For example, if you have the bundle at GameData/mystuff/mybundle.cargo, you would refer to the assets inside using a config file like this:

PART
{
  // ...
  MODEL
  {
    model = mystuff/mybundle/Sphere
    // ...
  }
  // ...
}

 

Link to comment
Share on other sites

  • 2 weeks later...

Hoooolly Kraken !!!

MY TEST MODEL WORKED. It has a prefab built as "variant" from custom FBX imported model and a vanilla Diffuse shader with custom png texture.

Next test is animation driving a custom shader (which is already proven to work since prefab holds a complete compiled shader, does not matter if it is built into KSP ).

A noteworthy reminder : in my part config i set model with "mesh" value instead of MODEL{} node so model loading failed every time before i changed it to the above config style.

One more thing : i've "discovered" BlendShapes. That thing is really something else. Pretty mych every inflatable part uses this method to show part inflating animation. I'll HAVE to try that next.

Oh also i didn't have time to test the code, that's why this reply is soooo late. THANKS @xEvilReeperx , you pretty much wrote the entire plugin for me. This is something i don't see every day !

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...