에셋 번들을 사용하면 에셋이 중복되는 문제가 발생할 수 있다.

이는 메모리 프로파일러를 통해서 확인할 수 있다.

 

여태까지 열심히 알아본 바로는 중복이 발생하는 경우는 다음과 같다.

 

1. 에셋 번들의 종속성(Dependencies)이 제대로 설정되지 않은 경우

에셋 번들의 종속성이 설정되어 있다면 번들 밖의 에셋을 사용할 때 종속된 에셋 번들을 먼저 로드하고, 해당 에셋에 있는 에셋을 사용한다. 종속성이 잘 지정되어 있다면, Shader를 사용한다고 가정하면 종속된 에셋 번들의 Shader 1개를 공유해서 사용한다.

에셋 번들의 종속성이 설정되어 있지 않다면, 번들 밖의 에셋을 사용할 때 에셋을 복제해서 번들 내부에 포함시킨다. 이 에셋을 다른 에셋 번들이 참조할 수 없기 때문에 이런 복제를 반복하는 에셋 번들의 수만큼 에셋이 복제된다. 이 경우 똑같은 Shader가 10개 이상 메모리에 적재되는 경우도 생긴다.

 

Tales from the optimization trenches: Saving memory with Addressables | Unity Blog

 

Tales from the optimization trenches: Saving memory with Addressables | Unity Blog

Problem: If we instantiate all of our items and despawn the boss sword, we will still see the boss sword’s texture “BossSword_E ” in memory, even though it isn’t in use. The reason for this is that, while you can partially load asset bundles, it

blog.unity.com

만약 이해가 되지 않는다면 해당 내용을 참고하면 쉽게 이해할 수 있을 것이다.

Addressable에 대한 소개지만, 에셋 번들을 기반으로 하고 있기 떄문에 Assetbundle에도 대부분 유효한 내용이다.

 

1-1. 유니티를 처음 실행하면 유니티에서 Normal 관련 문제에 대해서 다이얼로그를 띄우는 경우가 있는데, 여기서 Fix Now가 아니라 Ignore를 선택한 경우.

AssetBundles missing dependency to other AssetBundle - Unity Forum

 

AssetBundles missing dependency to other AssetBundle

Hey everyone, I'm running into a very strange issue that I've been trying to solve for days now. A high level of what we do for our bundle process: We...

forum.unity.com

에셋 번들을 빌드할 때는 에디터가 Fix Now를 선택하면서 Meta 파일을 변경하고, 이로 인해 종속성이 끊어질 수 있다고 한다.

 

1-2. BuildPipeline을 사용하면서 각각의 에셋을 다른 방식으로 압축하는 경우

Unity - Scripting API: BuildPipeline.BuildAssetBundles (unity3d.com)

 

Unity - Scripting API: BuildPipeline.BuildAssetBundles

This signature of BuildAssetBundles is recommended and exposes the most functionality. The other signatures, documented below, are retained for backward compatibility and convenience. During the AssetBundle build process, classes that implement IProcessSce

docs.unity3d.com

위 링크에서

public static AssetBundleManifest BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

가 있는데, BuildAssetBundleOptions를 받아서 빌드를 한다.

여기에는 비트 연산자를 이용해서 여러 옵션들을 포함시킬 수 있는데, A, B, C 에셋을 LZ4로 압축하고 D, E, F 에셋을 LZMA로 압축한다면 (A, B, C)로 빌드를 1번, (D, E, F)로 빌드를 1번 거치게 된다.

때문에 빌드 과정이 2번으로 분리되고, (A, B, C) 안에서는 Dependencies가 정상적으로 설정되지만 (A, B, C)와 (D, E, F)간의 Dependencies는 기록되지 않는다.

 

Mixing Asset Bundle Compression Type - Unity Forum

 

Mixing Asset Bundle Compression Type

Does anyone know if it is possible to mix AssetBundle compression types inside a single app? We have a mix of AssetBundles some of which are deployed...

forum.unity.com

이에 대한 내용이 유니티 포럼에 나와있다.

나도 압축 방식을 2개를 사용하는 것이 문제라는 것을 인지한 이후에 검색하여 겨우겨우 찾은 내용이라 시간을 꽤나 허비했다.

 

이 글에서 나온 해결책은 Scriptable Build Pipeline을 사용하라는 것.

 

Usage Examples | Scriptable Build Pipeline | 1.21.8 (unity3d.com)

 

Usage Examples | Scriptable Build Pipeline | 1.21.8

Usage Examples Basic Example This example assumes that your are already familiar with the basic usage of BuildPipeline.BuildAssetBundles and want to switch to using Scriptable Build Pipeline with as little effort as possible. The following code example sho

docs.unity3d.com

링크 내부에 Per-Bundle Compression Example이 나와있는데, 해당 부분을 참고하면 된다.

 

using UnityEditor;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline;
using UnityEditor.Build.Pipeline.Interfaces;

public static class BuildAssetBundlesExample
{
    // New parameters class inheriting from BundleBuildParameters
    class CustomBuildParameters : BundleBuildParameters
    {
        public Dictionary<string, BuildCompression> PerBundleCompression { get; set; }

        public CustomBuildParameters(BuildTarget target, BuildTargetGroup group, string outputFolder) : base(target, group, outputFolder)
        {
            PerBundleCompression = new Dictionary<string, BuildCompression>();
        }

        // Override the GetCompressionForIdentifier method with new logic
        public override BuildCompression GetCompressionForIdentifier(string identifier)
        {
            BuildCompression value;
            if (PerBundleCompression.TryGetValue(identifier, out value))
                return value;
            return BundleCompression;
        }
    }

    public static bool BuildAssetBundles(string outputPath, bool useChunkBasedCompression, BuildTarget buildTarget, BuildTargetGroup buildGroup)
    {
        var buildContent = new BundleBuildContent(ContentBuildInterface.GenerateAssetBundleBuilds());
        // Construct the new parameters class
        var buildParams = new CustomBuildParameters(buildTarget, buildGroup, outputPath);
        // Populate the bundle specific compression data
        buildParams.PerBundleCompression.Add("Bundle1", BuildCompression.DefaultUncompressed);
        buildParams.PerBundleCompression.Add("Bundle2", BuildCompression.DefaultLZMA);

        if (m_Settings.compressionType == CompressionType.None)
            buildParams.BundleCompression = BuildCompression.DefaultUncompressed;
        else if (m_Settings.compressionType == CompressionType.Lzma)
            buildParams.BundleCompression = BuildCompression.DefaultLZMA;
        else if (m_Settings.compressionType == CompressionType.Lz4 || m_Settings.compressionType == CompressionType.Lz4HC)
            buildParams.BundleCompression = BuildCompression.DefaultLZ4;

        IBundleBuildResults results;
        ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParams, buildContent, out results);
        return exitCode == ReturnCode.Success;
    }
}

내부에 요런 코드가 있는데, BundleBuildParameters를 상속받은 CustomBuildParameters Class를 정의한다.

 

안에 있는 PerBundleCompression에는 에셋 번들의 이름과 해당 번들의 압축 방식을 저장하면 된다.

 

	public override BuildCompression GetCompressionForIdentifier(string identifier)
        {
            BuildCompression value;
            if (PerBundleCompression.TryGetValue(identifier, out value))
                return value;
            return BundleCompression;
        }

또, 내부에는 GetCompressionForIdentifier가 override되어 선언되어 있는데, ContentPipeline.BuildAssetBundles는 빌드를 할 때 번들마다 GetCompressionForIdentifier()를 호출해서 압축 방식을 판단한다. (해당 함수 내부에서 Log를 찍어주면 빌드 이후에 번들마다 호출되는 것을 확인할 수 있다.) 압축 방식이 따로 지정되어있지 않으면 BundleCompression을 반환한다. BundleCompression은 Default 압축 방식을 넣어주면 된다.

 

이렇게 하면 압축 방식이 달라서 생기는 Dependencies 관련 문제는 말끔히 해결이 된다.

단, BuildPipeline과 다르기 때문에 주의해야할 점이 몇 가지 있다.

 

1-2-1. Scriptable Build Pipeline을 사용하면 BuildePipeline과는 다르게 Manifest파일이 나오지 않는다.

기존에 Manifest 파일을 사용하여 처리를 하고 있던 부분이 있다면, 그 부분들을 전부 수정해줘야 한다.

IBundleBuildResults인 results에 Manifest에 기록되던 Dependencies나, Hash, CRC 등의 값이 저장되기 때문에 해당 정보를 따로 파일에 저장해서 활용하면 된다.

 

1-2-2. Scriptable Build Pipeline을 사용하여 에셋을 빌드하면, 에셋을 로드할 때 대소문자를 엄격하게 구분한다.

아마 대소문자가 다른 에셋을 만들 것을 염두에 둔 것인지는 모르겠는데... SBP is strict about the rule: "what you pass in is exactly what you get out". 라고 하니... 뭐 그런가보다.

AssetBundle Browser로 확인해보면 BuildPipeline으로 빌드한 경우 에셋의 Path가 전부 소문자로 되어있는데, ContentPipeline으로 빌드한 경우 실제 경로와 대소문자 구분이 똑같다.

 

1-2-3. Scriptable Build Pipeline을 사용하면 빌드한 에셋 번들을 삭제해도 다시 빌드하지 않고, 캐시를 활용한다.

일반 BuildPipeline을 사용하면 빌드된 번들을 삭제하면 처음부터 다시 빌드를 하는 것 같은데, ContentPipeline의 경우 Library/BuildCache에 빌드 정보가 캐시되어 빌드를 굉장히 금방 끝낸다. 다만 여기서 문제가 바뀐 부분만 빌드를 하기 때문에 쉐이더를 컴파일하지 않아서 Shader Stripping을 수정해도 적용되지 않을 수 있다. 캐시 정보를 삭제하거나, 이건 번거로우니 IBuildParameters.UseCache = false; 를 사용해서 강제로 처음부터 다시 빌드를 하게 만들어주면 된다. 

 

Upgrade Guide | Scriptable Build Pipeline | 1.21.8 (unity3d.com)

 

Upgrade Guide | Scriptable Build Pipeline | 1.21.8

Upgrade Guide To build your AssetBundles with the SBP package, use the CompatibilityBuildPipeline.BuildAssetBundles method wherever you used the BuildPipeline.BuildAssetBundle method. Note: Not all of the features that were supported previously are support

docs.unity3d.com

이외의 자세한 내용은 위 링크를 참조하시라.

 

1-2-4. Shader Stripping에서 특정 부분이 BuildPipeline과 다르게 작동하는 것 같다. (추측)

Question - SBP don't strip shader variants? - Unity Forum

 

Question - SBP don't strip shader variants?

I'm upgrading asset bundle build method to scriptable build pipeline, but the result shows shader bundle size grown from 5MB to 22MB, and deep inside...

forum.unity.com

SBP(Scritable Build Pipeline)의 Shader Stripping에 대한 포럼의 글이 있다.

나의 경우, 기존에는 70MB 수준이던 Shader 총 용량이 210MB로 증가했다.

나도 해당 내용을 참고해서 BuildPipeline을 사용했을 때와 ContentPipeline을 사용했을 때 남은 Keywords를 비교해봤다.

FOG_EXP2와 SHADOWMASK? 2개의 키워드가 추가로 엄청나게 많은 비중을 차지하는 것을 발견했고, 해당 키워드들을 Shader Stripping 목록에 추가해서 지웠고, 90MB로 용량을 대폭 줄일 수 있었다.

처음에는 Player Settings의 Graphics내의 Shader Stripping에 있는 키워드들이라 여기서 지워주는 처리를 안 해주는 것이 아닌가, 생각을 했는데 아까 실험을 몇 가지 해보니 FOG_EXP는 제대로 지워주는 것 같아서 굉장히 헷갈린다. 월요일에 한 번 더 확인해볼 생각이다.

 

하지만 여전히 90MB로 기존에 비해 30% 가량 큰 용량을 차지하고 있는데, 일부 쉐이더의 용량이 다르다는 문제가 있다. ShaderX라는, Shader를 암호화 해주는? 기능을 사용하는 Shader인데, 분명 사용하는 키워드의 수가 이전과 동일하고 Pass 수도 동일한데 용량 차이가 2배 이상 난다. Pass 내에서 사용하는 키워드의 수가 차이가 나는 건가? 싶기도 한데... log shader compilation 기능을 사용해서 확인해보기는 했지만 경우에 따른 변수가 있어서 다시 확인을 해봐야 할 것 같다. IPreprocessShaders.OnProcessShader에서도 확인해봤지만 BuildPipeline은 이게 요상하게 많이 호출되어서 확인하기도 어렵다. RenderDoc도 사용해봤지만 별 차이를 발견하지는 못했다. 아무튼 용량에 차이가 있다는 것은 Shader Stripping의 방식에 뭔가 차이가 있다는 것인데 Shader 내부를 직접 볼 수가 없어서 정보가 굉장히 부족하다보니 하나씩 삽질을 해가면서 파악을 하고 있다. 이 문제를 해결하면 아마 여기에 추가로 적을 것이라고 생각한다. 위의 포럼에도 내용을 보탤 수 있으면 좋을 것 같다. 아마도 ContentPipeline이 추가된 이후에, Addressable이 추가되어서 ContentPipeline에 대한 지원이 부실하고, 사용하는 사람의 수가 적기 때문에 내용이 적은 것 같은데... Addressable로 업그레이드 할 수 있으면 굉장히 좋겠다는 생각이 든다.

 

내용을 쓰다보니 Shader Stripping에 대한 내용도 적으면 좋겠다는 생각이 들기는 하는데, 현재 사용하는 Shader Stripping에 대한 기능을 업그레이드 할 계획을 세우고 있어서 해당 시스템을 구축하고 나서 내용을 적을 예정이다.

 

Bug - (Case 1321433) Addressables/SBP don't detect IPreprocessShaders changes - Unity Forum

 

Bug - (Case 1321433) Addressables/SBP don't detect IPreprocessShaders changes

Adding and changing an IPreprocessShaders implementation doesn't cause Addressables/SBP to rebuild shaders. You have to purge the build cache for...

forum.unity.com

+Scriptable Build Pipeline을 사용하면 IPreprocessShaders가 제대로 호출되지 않는데, SBP는 기존의 BuildPipeline과는 다르게 Library/BuildCache 폴더에 캐시를 해두고 빌드를 하기 때문에 빌드한 에셋 번들을 날려도 캐시된 데이터를 바탕으로 에셋 번들을 빌드하기 때문인 것으로 생각된다. 때문에 Shader가 바뀌지 않는 이상 Shader를 다시 컴파일하지 않고, 이에 따라 IPreProcessShaders가 제대로 호출되지 않는 것이다. 이를 해결하기 위해서는 BuildCache 폴더를 삭제한 후에 다시 빌드할 수도 있지만, 이 폴더는 파일 수가 굉장히 많고 용량이 크기 때문에 IBuildParameters.UseCache = false; 를 해주면 조금 더 간편하게 IPreProcessShaders를 호출할 수 있다.

 

 

2. 에셋 번들을 잘못 Unload 한 경우

 

AssetBundleを完全に理解する - Qiita

 

AssetBundleを完全に理解する - Qiita

はじめに前回の記事では、Unityにおけるリソース読み込みについての基本的な知識を総ざらいし、ResourcesやAssetBundleの特徴や違いについて取り上げ、AssetBundleの必要性…

qiita.com

위 링크에 위와 같은 내용이 있다.

assetBundle.Unload(false)를 하면 로드된 리소스를 유지한 채로 meta 정보를 날리는데, 이 meta 정보에는 리소스가 메모리 상 어디에 로드되었는지가 기록되기 때문에 이걸 날려버리고 다시 로드를 하면 기존의 리소스를 참조하지 못하고 새로운 리소스를 로드하게 된다.(라고 들었다) 즉, 메모리 상에 에셋이 중복되어 생성되는 것이다.

assetBundle.Unload(true)를 하면 로드된 리소스까지 같이 날려버리지만, 사용하고 있는 경우 마젠타 색으로 표시될 것이다.

즉, 어느 경우든 "해제하는 에셋 번들에서 에셋을 사용하고 있지 않다" 는 것이 확실해야 문제가 발생하지 않는다.

이것을 확인할 수 있는 방법은 Reference Count를 기록하여 0이 되었는지 확인하는 것인데, BuildPipeline 등은 이 기능을 코드로 직접 구현해줘야 한다. 참고로 Addressable은 이걸 알아서 해주면서 참조가 0이되면 알아서 해제까지 해주기 때문에 Addressable을 사용하는 것이 굉장히 좋겠다.

 

다음 주 회사에서 하는 스터디 내용으로 Addressable에 대한 내용을 준비해 갈 예정인데, 여태 알아본 바로는 우리가 사용하는 기본 AssetBundle의 굉장한 상위 호환으로 느껴진다. 준비를 잘 해가서 Addressable로 업그레이드하면 굉장히 좋겠다는 점을 어필하고 싶다. 아마 Addressable에 대해 공부한 내용을 블로그에도 적을 것 같다.

+ Recent posts