I created interactive Azur Lane Live2D viewer

I’m working on a Live2D viewer for Azur Lane. Not just a regular Live2D viewer, I want an experience that is similar to the content in the game.

I’ve done similar projects for other games, but they used Spine technology. Azur Lane uses Cubism, which I’ve only recently learned about.

I’ve encountered some obstacles in the development. If anyone here knows more about Cubism, you can help me at GitHub - bungaku-moe/Azure-Gravure: ⚓ Azur Lane Interactive Live2D/Spine Viewer. The project is open source.

Here’s a small glimpse of what I’ve achieved: https://www.youtube.com/watch?v=DGpxtZsssYs

2 个赞

主要问题是现在的AssetStudio或UnityLive2DExtractor解包出来的动作文件缺少一些参数,没有完美还原。建议弄个直接读取unity文件的

Of course there are some missing things like the Events name, but overall I can get it back. It’s just that I’m having trouble with why the animation doesn’t work when running in the build version, while in the Editor it’s fine.

Just so you know, I can make it work by using the generated prefab when you importing .moc3 file in Unity Editor. But it defeats the purpose to dynamically instantiate the model at runtime.

I’ve tried asking in the official Live2D forum, I hope to get an answer or useful tips.

git 上有几个unity的案例,

instantiate the model at runtime

CubismModel3Json 中有这一段

    /// <summary>
    /// Instantiates a <see cref="CubismMoc">model source</see> and a <see cref="CubismModel">model</see> with the default texture set.
    /// </summary>
    /// <param name="shouldImportAsOriginalWorkflow">Should import as original workflow.</param>
    /// <returns>The instantiated <see cref="CubismModel">model</see> on success; <see langword="null"/> otherwise.</returns>
    public CubismModel ToModel(bool shouldImportAsOriginalWorkflow = false)
    {
        return ToModel(CubismBuiltinPickers.MaterialPicker, CubismBuiltinPickers.TexturePicker, shouldImportAsOriginalWorkflow);
    }

    /// <summary>
    /// Instantiates a <see cref="CubismMoc">model source</see> and a <see cref="CubismModel">model</see>.
    /// </summary>
    /// <param name="pickMaterial">The material mapper to use.</param>
    /// <param name="pickTexture">The texture mapper to use.</param>
    /// <param name="shouldImportAsOriginalWorkflow">Should import as original workflow.</param>
    /// <returns>The instantiated <see cref="CubismModel">model</see> on success; <see langword="null"/> otherwise.</returns>
    public CubismModel ToModel(MaterialPicker pickMaterial, TexturePicker pickTexture, bool shouldImportAsOriginalWorkflow = false)
    {
        // Initialize model source and instantiate it.
        var mocAsBytes = Moc3;


        if (mocAsBytes == null)
        {
            return null;
        }


        var moc = CubismMoc.CreateFrom(mocAsBytes);


        var model = CubismModel.InstantiateFrom(moc);

        if (model == null)
        {
            return null;
        }

        model.name = Path.GetFileNameWithoutExtension(FileReferences.Moc);

#if UNITY_EDITOR
// Add parameters and parts inspectors.
model.gameObject.AddComponent();
model.gameObject.AddComponent();
#endif

        // Create renderers.
        var rendererController = model.gameObject.AddComponent<CubismRenderController>();
        var renderers = rendererController.Renderers;

        var drawables = model.Drawables;

        if (renderers == null || drawables  == null)
        {
            return null;
        }

        // Initialize materials.
        for (var i = 0; i < renderers.Length; ++i)
        {
            renderers[i].Material = pickMaterial(this, drawables[i]);
        }


        // Initialize textures.
        for (var i = 0; i < renderers.Length; ++i)
        {
            renderers[i].MainTexture = pickTexture(this, drawables[i]);
        }


        if (model.Parts != null)
        {
            var parts = model.Parts;

            // Create and initialize partColorsEditors.
            for (int i = 0; i < parts.Length; i++)
            {
                var partColorsEditor = parts[i].gameObject.AddComponent<CubismPartColorsEditor>();
                partColorsEditor.TryInitialize(model);
            }
        }


        // Initialize drawables.
        if (HitAreas != null)
        {
            for (var i = 0; i < HitAreas.Length; i++)
            {
                for (var j = 0; j < drawables.Length; j++)
                {
                    if (drawables[j].Id == HitAreas[i].Id)
                    {
                        // Add components for hit judgement to HitArea target Drawables.
                        var hitDrawable = drawables[j].gameObject.AddComponent<CubismHitDrawable>();
                        hitDrawable.Name = HitAreas[i].Name;

                        drawables[j].gameObject.AddComponent<CubismRaycastable>();
                        break;
                    }
                }
            }
        }

        //Load from cdi3.json
        var DisplayInfo3JsonAsString = DisplayInfo3Json;
        var cdi3Json = CubismDisplayInfo3Json.LoadFrom(DisplayInfo3JsonAsString);

        // Initialize groups.
        var parameters = model.Parameters;

        for (var i = 0; i < parameters.Length; ++i)
        {
            if (IsParameterInGroup(parameters[i], "EyeBlink"))
            {
                if (model.gameObject.GetComponent<CubismEyeBlinkController>() == null)
                {
                    model.gameObject.AddComponent<CubismEyeBlinkController>();
                }


                parameters[i].gameObject.AddComponent<CubismEyeBlinkParameter>();
            }


            // Set up mouth parameters.
            if (IsParameterInGroup(parameters[i], "LipSync"))
            {
                if (model.gameObject.GetComponent<CubismMouthController>() == null)
                {
                    model.gameObject.AddComponent<CubismMouthController>();
                }


                parameters[i].gameObject.AddComponent<CubismMouthParameter>();
            }


            // Setting up the parameter name for display.
            if (cdi3Json != null)
            {
                var cubismDisplayInfoParameterName = parameters[i].gameObject.AddComponent<CubismDisplayInfoParameterName>();
                cubismDisplayInfoParameterName.Name = parameters[i].Id;
                for (int j = 0; j < cdi3Json.Parameters.Length; j++)
                {
                    if (cdi3Json.Parameters[j].Id == parameters[i].Id)
                    {
                        cubismDisplayInfoParameterName.Name = cdi3Json.Parameters[j].Name;
                        break;
                    }
                }
                cubismDisplayInfoParameterName.DisplayName = string.Empty;
            }
        }

        if (cdi3Json != null)
        {
            // Setting up the part name for display.
            // Initialize groups.
            var parts = model.Parts;

            for (var i = 0; i < parts.Length; i++)
            {
                var cubismDisplayInfoPartNames = parts[i].gameObject.AddComponent<CubismDisplayInfoPartName>();
                cubismDisplayInfoPartNames.Name = parts[i].Id;
                for (int j = 0; j < cdi3Json.Parts.Length; j++)
                {
                    if (cdi3Json.Parts[j].Id == parts[i].Id)
                    {
                        cubismDisplayInfoPartNames.Name = cdi3Json.Parts[j].Name;
                        break;
                    }
                }
                cubismDisplayInfoPartNames.DisplayName = string.Empty;
            }

            // Get combined parameter information
            var combinedParameters = cdi3Json.CombinedParameters;

            if (combinedParameters != null)
            {
                // Parameters are always combined in pairs of two.
                const int combinedParameterCount = 2;

                // Set up CubismDisplayInfoCombinedParameterInfo component.
                var combinedParameterInfo = model.gameObject.AddComponent<CubismDisplayInfoCombinedParameterInfo>();
                combinedParameterInfo.CombinedParameters = new CubismDisplayInfo3Json.CombinedParameter[combinedParameters.Length];

                for (var index = 0; index < combinedParameters.Length; index++)
                {
                    // Skip if the combined parameter is invalid.
                    if (combinedParameters[index].Ids == null || combinedParameters[index].Ids.Length != combinedParameterCount)
                    {
                        Debug.LogWarning($"The data contains invalid CombinedParameters in {model.Moc.name}.cdi3.json.");
                        continue;
                    }

                    var combinedParameterIds = combinedParameters[index].Ids;

                    // Set CombinedParameter.
                    combinedParameterInfo.CombinedParameters[index] = new CubismDisplayInfo3Json.CombinedParameter
                    {
                        HorizontalParameterId = combinedParameterIds[0],
                        VerticalParameterId = combinedParameterIds[1]
                    };
                }
            }
        }

        // Add mask controller if required.
        for (var i = 0; i < drawables.Length; ++i)
        {
            if (!drawables[i].IsMasked)
            {
                continue;
            }


            // Add controller exactly once...
            model.gameObject.AddComponent<CubismMaskController>();


            break;
        }

        // Add original workflow component if is original workflow.
        if(shouldImportAsOriginalWorkflow)
        {
            // Add cubism update manager.
            var updateManager = model.gameObject.GetComponent<CubismUpdateController>();

            if(updateManager == null)
            {
                model.gameObject.AddComponent<CubismUpdateController>();
            }

            // Add parameter store.
            var parameterStore = model.gameObject.GetComponent<CubismParameterStore>();

            if(parameterStore == null)
            {
                parameterStore = model.gameObject.AddComponent<CubismParameterStore>();
            }

            // Add pose controller.
            var poseController = model.gameObject.GetComponent<CubismPoseController>();

            if(poseController == null)
            {
                poseController = model.gameObject.AddComponent<CubismPoseController>();
            }

            // Add expression controller.
            var expressionController = model.gameObject.GetComponent<CubismExpressionController>();

            if(expressionController == null)
            {
                expressionController = model.gameObject.AddComponent<CubismExpressionController>();
            }


            // Add fade controller.
            var motionFadeController = model.gameObject.GetComponent<CubismFadeController>();

            if(motionFadeController == null)
            {
                motionFadeController = model.gameObject.AddComponent<CubismFadeController>();
            }

        }


        // Initialize physics if JSON exists.
        var physics3JsonAsString = Physics3Json;


        if (!string.IsNullOrEmpty(physics3JsonAsString))
        {
            var physics3Json = CubismPhysics3Json.LoadFrom(physics3JsonAsString);
            var physicsController = model.gameObject.GetComponent<CubismPhysicsController>();

            if (physicsController == null)
            {
                physicsController = model.gameObject.AddComponent<CubismPhysicsController>();

            }

            physicsController.Initialize(physics3Json.ToRig());
        }


        var userData3JsonAsString = UserData3Json;


        if (!string.IsNullOrEmpty(userData3JsonAsString))
        {
            var userData3Json = CubismUserData3Json.LoadFrom(userData3JsonAsString);


            var drawableBodies = userData3Json.ToBodyArray(CubismUserDataTargetType.ArtMesh);

            for (var i = 0; i < drawables.Length; ++i)
            {
                var index = GetBodyIndexById(drawableBodies, drawables[i].Id);

                if (index >= 0)
                {
                    var tag = drawables[i].gameObject.GetComponent<CubismUserDataTag>();


                    if (tag == null)
                    {
                        tag = drawables[i].gameObject.AddComponent<CubismUserDataTag>();
                    }


                    tag.Initialize(drawableBodies[index]);
                }
            }
        }

        if (model.gameObject.GetComponent<Animator>() == null)
        {
            model.gameObject.AddComponent<Animator>();
        }

        // Make sure model is 'fresh'
        model.ForceUpdateNow();


        return model;
    }

It’s not an instantiation issue. Based from the answer by Live2D staff, I can’t create a non-legacy AnimationClip at runtime which utilize Animator/Playable Graph. I must work with the legacy Animation component if I want to generate the AnimationClip at runtime.

Legacy Animation component have some pretty limitations. In the meantime, I’ll try playing with it if it’s fit or not for my use cases.

I did it guys! I fixed the issue :grin: