앱인토스 개발자센터 로고
Skip to content

렌더링 최적화

앱인토스 Unity 게임의 렌더링 성능을 극대화하기 위한 포괄적인 최적화 기법과 실무 가이드를 제공해요.


1. 드로우콜 최적화

배치 렌더링 시스템

c#
public class AppsInTossDrawCallOptimizer : MonoBehaviour
{
    [System.Serializable]
    public class BatchGroup
    {
        public string groupName;
        public Material sharedMaterial;
        public List<MeshRenderer> renderers = new List<MeshRenderer>();
        public bool enableInstancing = true;
        public int maxInstancesPerBatch = 1000;
    }
    
    [Header("드로우콜 최적화 설정")]
    public BatchGroup[] batchGroups;
    public bool enableAutoBatching = true;
    public bool enableGPUInstancing = true;
    public bool enableStaticBatching = true;
    
    [Header("앱인토스 최적화")]
    public int targetDrawCallsPerFrame = 50; // 앱인토스 권장 제한
    public bool adaptToPerformance = true;
    
    private Dictionary<Material, List<MeshRenderer>> materialGroups = new Dictionary<Material, List<MeshRenderer>>();
    private int currentDrawCalls = 0;
    
    void Start()
    {
        InitializeDrawCallOptimization();
        OptimizeStaticGeometry();
        StartCoroutine(MonitorDrawCalls());
    }
    
    void InitializeDrawCallOptimization()
    {
        // 씬의 모든 렌더러를 머티리얼별로 그룹화
        GroupRenderersByMaterial();
        
        // 배치 그룹 설정
        SetupBatchGroups();
        
        // GPU 인스턴싱 설정
        if (enableGPUInstancing)
        {
            SetupGPUInstancing();
        }
        
        Debug.Log($"드로우콜 최적화 초기화 완료 - 목표: {targetDrawCallsPerFrame} 드로우콜/프레임");
    }
    
    void GroupRenderersByMaterial()
    {
        var allRenderers = FindObjectsOfType<MeshRenderer>();
        
        foreach (var renderer in allRenderers)
        {
            var material = renderer.sharedMaterial;
            if (material != null)
            {
                if (!materialGroups.ContainsKey(material))
                {
                    materialGroups[material] = new List<MeshRenderer>();
                }
                materialGroups[material].Add(renderer);
            }
        }
        
        Debug.Log($"머티리얼 그룹화 완료: {materialGroups.Count}개 그룹");
    }
    
    void SetupBatchGroups()
    {
        foreach (var group in batchGroups)
        {
            if (group.sharedMaterial == null) continue;
            
            // 같은 머티리얼을 사용하는 렌더러들을 배치 그룹에 추가
            if (materialGroups.ContainsKey(group.sharedMaterial))
            {
                group.renderers.AddRange(materialGroups[group.sharedMaterial]);
                
                // 배치 최적화 적용
                OptimizeBatchGroup(group);
            }
        }
    }
    
    void OptimizeBatchGroup(BatchGroup group)
    {
        if (group.renderers.Count <= 1) return;
        
        // 렌더러들의 메시를 결합할 수 있는지 확인
        var meshFilters = group.renderers
            .Select(r => r.GetComponent<MeshFilter>())
            .Where(mf => mf != null && mf.sharedMesh != null)
            .ToArray();
        
        if (meshFilters.Length < 2) return;
        
        // 정적 배칭 적용
        if (enableStaticBatching && AreRenderersStatic(group.renderers))
        {
            ApplyStaticBatching(group, meshFilters);
        }
        // 동적 배칭 적용
        else if (enableAutoBatching)
        {
            ApplyDynamicBatching(group);
        }
    }
    
    bool AreRenderersStatic(List<MeshRenderer> renderers)
    {
        return renderers.All(r => r.gameObject.isStatic);
    }
    
    void ApplyStaticBatching(BatchGroup group, MeshFilter[] meshFilters)
    {
        // Unity의 정적 배칭 사용
        var gameObjects = meshFilters.Select(mf => mf.gameObject).ToArray();
        StaticBatchingUtility.Combine(gameObjects, gameObjects[0].transform.parent?.gameObject);
        
        Debug.Log($"정적 배칭 적용: {group.groupName} - {gameObjects.Length}개 오브젝트");
    }
    
    void ApplyDynamicBatching(BatchGroup group)
    {
        // 동적 배칭을 위한 설정 최적화
        foreach (var renderer in group.renderers)
        {
            // 머티리얼 인스턴스화 방지
            if (renderer.material != group.sharedMaterial)
            {
                renderer.sharedMaterial = group.sharedMaterial;
            }
            
            // 스케일 정규화 (동적 배칭 조건)
            if (renderer.transform.localScale != Vector3.one)
            {
                var scale = renderer.transform.localScale;
                if (Mathf.Abs(scale.x - scale.y) < 0.01f && 
                    Mathf.Abs(scale.y - scale.z) < 0.01f)
                {
                    renderer.transform.localScale = Vector3.one * scale.x;
                }
            }
        }
        
        Debug.Log($"동적 배칭 최적화: {group.groupName}");
    }
    
    void SetupGPUInstancing()
    {
        foreach (var group in materialGroups)
        {
            var material = group.Key;
            var renderers = group.Value;
            
            if (renderers.Count > 10 && CanUseInstancing(renderers))
            {
                EnableMaterialInstancing(material);
                SetupInstancedRendering(renderers);
            }
        }
    }
    
    bool CanUseInstancing(List<MeshRenderer> renderers)
    {
        if (renderers.Count < 2) return false;
        
        // 같은 메시를 사용하는지 확인
        var firstMesh = renderers[0].GetComponent<MeshFilter>()?.sharedMesh;
        if (firstMesh == null) return false;
        
        return renderers.All(r => {
            var mf = r.GetComponent<MeshFilter>();
            return mf != null && mf.sharedMesh == firstMesh;
        });
    }
    
    void EnableMaterialInstancing(Material material)
    {
        // GPU 인스턴싱 키워드 활성화
        material.EnableKeyword("_INSTANCING_ON");
        material.enableInstancing = true;
        
        // 앱인토스 특화 키워드
        material.EnableKeyword("APPS_IN_TOSS_INSTANCED");
    }
    
    void SetupInstancedRendering(List<MeshRenderer> renderers)
    {
        // 인스턴스 데이터 준비
        var matrices = new List<Matrix4x4>();
        var colors = new List<Vector4>();
        
        foreach (var renderer in renderers)
        {
            matrices.Add(renderer.transform.localToWorldMatrix);
            colors.Add(renderer.material.color);
            
            // 원본 렌더러는 비활성화
            renderer.enabled = false;
        }
        
        // 인스턴스드 렌더링 설정
        CreateInstancedRenderer(renderers[0], matrices, colors);
    }
    
    void CreateInstancedRenderer(MeshRenderer template, List<Matrix4x4> matrices, List<Vector4> colors)
    {
        var instancedGO = new GameObject($"Instanced_{template.name}");
        var instancedRenderer = instancedGO.AddComponent<InstancedMeshRenderer>();
        
        instancedRenderer.Initialize(
            template.GetComponent<MeshFilter>().sharedMesh,
            template.sharedMaterial,
            matrices.ToArray(),
            colors.ToArray()
        );
    }
    
    void OptimizeStaticGeometry()
    {
        if (!enableStaticBatching) return;
        
        // 정적 오브젝트들을 찾아 배칭
        var staticObjects = FindObjectsOfType<GameObject>()
            .Where(go => go.isStatic && go.GetComponent<MeshRenderer>() != null)
            .ToArray();
        
        if (staticObjects.Length > 1)
        {
            StaticBatchingUtility.Combine(staticObjects, null);
            Debug.Log($"정적 배칭 완료: {staticObjects.Length}개 오브젝트");
        }
    }
    
    System.Collections.IEnumerator MonitorDrawCalls()
    {
        while (true)
        {
            yield return new WaitForSeconds(1f);
            
            // 현재 드로우콜 수 확인 (Unity Stats 활용)
            int currentDrawCalls = GetCurrentDrawCalls();
            
            if (currentDrawCalls > targetDrawCallsPerFrame && adaptToPerformance)
            {
                Debug.LogWarning($"드로우콜 수 초과: {currentDrawCalls} > {targetDrawCallsPerFrame}");
                ApplyEmergencyOptimization();
            }
            
            // 앱인토스 분석에 드로우콜 데이터 전송
            AppsInToss.ReportRenderingStats(currentDrawCalls, "draw_calls");
        }
    }
    
    int GetCurrentDrawCalls()
    {
        // Unity의 내부 통계를 통해 드로우콜 수 추정
        // 실제로는 Unity Profiler API를 사용해야 함
        return UnityEngine.Rendering.FrameDebugger.enabled ? 
            UnityEngine.Rendering.FrameDebugger.GetFrameEventCount() : 0;
    }
    
    void ApplyEmergencyOptimization()
    {
        Debug.Log("긴급 드로우콜 최적화 적용");
        
        // 가장 비효율적인 렌더러들 비활성화
        var renderers = FindObjectsOfType<MeshRenderer>()
            .Where(r => !IsEssentialRenderer(r))
            .OrderByDescending(r => GetRendererComplexity(r))
            .Take(10)
            .ToArray();
        
        foreach (var renderer in renderers)
        {
            renderer.enabled = false;
        }
        
        AppsInToss.ReportOptimizationApplied("emergency_drawcall_reduction", renderers.Length);
    }
    
    bool IsEssentialRenderer(MeshRenderer renderer)
    {
        // 필수 렌더러인지 판단 (UI, 플레이어, 토스 브랜딩 등)
        return renderer.gameObject.layer == LayerMask.NameToLayer("UI") ||
               renderer.name.Contains("Player") ||
               renderer.name.Contains("Toss") ||
               renderer.name.Contains("Essential");
    }
    
    float GetRendererComplexity(MeshRenderer renderer)
    {
        var meshFilter = renderer.GetComponent<MeshFilter>();
        if (meshFilter?.sharedMesh == null) return 0f;
        
        // 복잡도 계산 (버텍스 수 + 머티리얼 복잡도)
        float complexity = meshFilter.sharedMesh.vertexCount;
        
        if (renderer.sharedMaterial != null)
        {
            complexity += renderer.sharedMaterial.passCount * 100f;
            
            // 투명 머티리얼은 더 비용이 높음
            if (renderer.sharedMaterial.renderQueue >= 3000)
            {
                complexity *= 1.5f;
            }
        }
        
        return complexity;
    }
    
    // 공개 API
    public void ForceOptimization()
    {
        ApplyEmergencyOptimization();
    }
    
    public int GetCurrentBatchCount()
    {
        return batchGroups.Length;
    }
    
    public void AddToBatchGroup(string groupName, MeshRenderer renderer)
    {
        var group = System.Array.Find(batchGroups, g => g.groupName == groupName);
        if (group != null && !group.renderers.Contains(renderer))
        {
            group.renderers.Add(renderer);
            OptimizeBatchGroup(group);
        }
    }
}

// 인스턴스드 렌더링 컴포넌트
public class InstancedMeshRenderer : MonoBehaviour
{
    private Mesh mesh;
    private Material material;
    private Matrix4x4[] matrices;
    private Vector4[] colors;
    private MaterialPropertyBlock propertyBlock;
    
    public void Initialize(Mesh mesh, Material material, Matrix4x4[] matrices, Vector4[] colors)
    {
        this.mesh = mesh;
        this.material = material;
        this.matrices = matrices;
        this.colors = colors;
        
        propertyBlock = new MaterialPropertyBlock();
        propertyBlock.SetVectorArray("_InstanceColor", colors);
    }
    
    void Update()
    {
        if (mesh != null && material != null && matrices != null)
        {
            Graphics.DrawMeshInstanced(mesh, 0, material, matrices, matrices.Length, propertyBlock);
        }
    }
}

2. LOD 시스템 최적화

적응적 LOD 관리

c#
public class AppsInTossLODManager : MonoBehaviour
{
    [System.Serializable]
    public class LODSettings
    {
        public float[] distances = { 10f, 25f, 50f };
        public float[] qualityLevels = { 1.0f, 0.7f, 0.4f };
        public bool enableCulling = true;
        public float cullDistance = 100f;
    }
    
    [Header("LOD 설정")]
    public LODSettings lodSettings;
    public bool enableDynamicLOD = true;
    public bool adaptToPerformance = true;
    
    [Header("앱인토스 최적화")]
    public float performanceThreshold = 25f; // FPS
    public bool enableAggressiveLOD = true;
    
    private LODGroup[] lodGroups;
    private Camera mainCamera;
    private float[] originalDistances;
    
    void Start()
    {
        mainCamera = Camera.main;
        InitializeLODSystem();
        
        if (enableDynamicLOD)
        {
            InvokeRepeating(nameof(UpdateDynamicLOD), 1f, 1f);
        }
    }
    
    void InitializeLODSystem()
    {
        lodGroups = FindObjectsOfType<LODGroup>();
        originalDistances = new float[lodSettings.distances.Length];
        System.Array.Copy(lodSettings.distances, originalDistances, lodSettings.distances.Length);
        
        foreach (var lodGroup in lodGroups)
        {
            OptimizeLODGroup(lodGroup);
        }
        
        Debug.Log($"LOD 시스템 초기화: {lodGroups.Length}개 LOD 그룹");
    }
    
    void OptimizeLODGroup(LODGroup lodGroup)
    {
        var lods = lodGroup.GetLODs();
        
        for (int i = 0; i < lods.Length; i++)
        {
            if (i < lodSettings.distances.Length)
            {
                // 앱인토스 환경에 맞는 LOD 거리 조정
                float adjustedDistance = lodSettings.distances[i];
                
                // 모바일 환경에서는 더 가까운 거리에서 LOD 적용
                if (Application.isMobilePlatform)
                {
                    adjustedDistance *= 0.7f;
                }
                
                lods[i].screenRelativeTransitionHeight = adjustedDistance / 100f;
            }
            
            // LOD 레벨별 품질 조정
            OptimizeLODLevel(lods[i], i);
        }
        
        lodGroup.SetLODs(lods);
    }
    
    void OptimizeLODLevel(LOD lod, int level)
    {
        foreach (var renderer in lod.renderers)
        {
            if (renderer == null) continue;
            
            // LOD 레벨에 따른 머티리얼 품질 조정
            var material = renderer.sharedMaterial;
            if (material != null)
            {
                float qualityLevel = level < lodSettings.qualityLevels.Length ? 
                    lodSettings.qualityLevels[level] : 0.2f;
                
                SetMaterialQuality(material, qualityLevel);
            }
            
            // 그림자 설정 조정
            if (level > 0)
            {
                renderer.shadowCastingMode = level > 1 ? 
                    UnityEngine.Rendering.ShadowCastingMode.Off : 
                    UnityEngine.Rendering.ShadowCastingMode.On;
                
                renderer.receiveShadows = level == 0;
            }
        }
    }
    
    void SetMaterialQuality(Material material, float quality)
    {
        if (material.HasProperty("_QualityLevel"))
        {
            material.SetFloat("_QualityLevel", quality);
        }
        
        // 텍스처 품질 조정
        if (quality < 0.5f)
        {
            material.mainTextureScale = Vector2.one * 0.5f; // 텍스처 해상도 감소
        }
        else if (quality < 0.8f)
        {
            material.mainTextureScale = Vector2.one * 0.75f;
        }
    }
    
    void UpdateDynamicLOD()
    {
        if (!adaptToPerformance || mainCamera == null) return;
        
        // 현재 성능 확인
        float currentFPS = GetCurrentFPS();
        
        if (currentFPS < performanceThreshold)
        {
            ApplyAggressiveLOD();
        }
        else if (currentFPS > performanceThreshold * 1.5f)
        {
            RestoreNormalLOD();
        }
        
        // 카메라 위치 기반 동적 LOD 조정
        UpdateDistanceBasedLOD();
    }
    
    float GetCurrentFPS()
    {
        return 1.0f / Time.unscaledDeltaTime;
    }
    
    void ApplyAggressiveLOD()
    {
        Debug.Log("성능 부족으로 적극적 LOD 적용");
        
        // LOD 거리를 줄여서 더 빨리 낮은 품질로 전환
        for (int i = 0; i < lodSettings.distances.Length; i++)
        {
            lodSettings.distances[i] = originalDistances[i] * 0.6f;
        }
        
        UpdateAllLODGroups();
        
        AppsInToss.ReportOptimizationApplied("aggressive_lod", "성능 최적화");
    }
    
    void RestoreNormalLOD()
    {
        // 원래 LOD 설정 복원
        System.Array.Copy(originalDistances, lodSettings.distances, originalDistances.Length);
        UpdateAllLODGroups();
    }
    
    void UpdateAllLODGroups()
    {
        foreach (var lodGroup in lodGroups)
        {
            OptimizeLODGroup(lodGroup);
        }
    }
    
    void UpdateDistanceBasedLOD()
    {
        if (mainCamera == null) return;
        
        Vector3 cameraPos = mainCamera.transform.position;
        
        foreach (var lodGroup in lodGroups)
        {
            if (lodGroup == null) continue;
            
            float distance = Vector3.Distance(cameraPos, lodGroup.transform.position);
            
            // 매우 먼 거리의 오브젝트는 컬링
            if (lodSettings.enableCulling && distance > lodSettings.cullDistance)
            {
                SetLODGroupVisible(lodGroup, false);
            }
            else
            {
                SetLODGroupVisible(lodGroup, true);
                
                // 앱인토스 환경에서 거리별 추가 최적화
                if (distance > lodSettings.cullDistance * 0.8f)
                {
                    ApplyDistantOptimization(lodGroup);
                }
            }
        }
    }
    
    void SetLODGroupVisible(LODGroup lodGroup, bool visible)
    {
        var lods = lodGroup.GetLODs();
        foreach (var lod in lods)
        {
            foreach (var renderer in lod.renderers)
            {
                if (renderer != null)
                {
                    renderer.enabled = visible;
                }
            }
        }
    }
    
    void ApplyDistantOptimization(LODGroup lodGroup)
    {
        // 매우 먼 오브젝트에 대한 추가 최적화
        var lods = lodGroup.GetLODs();
        
        if (lods.Length > 0)
        {
            // 가장 낮은 LOD만 활성화
            var lowestLOD = lods[lods.Length - 1];
            
            foreach (var renderer in lowestLOD.renderers)
            {
                if (renderer != null)
                {
                    // 그림자 완전 비활성화
                    renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
                    renderer.receiveShadows = false;
                    
                    // 라이트맵만 사용
                    renderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
                }
            }
        }
    }
    
    // 공개 API
    public void SetLODQualityLevel(float quality)
    {
        quality = Mathf.Clamp01(quality);
        
        for (int i = 0; i < lodSettings.distances.Length; i++)
        {
            lodSettings.distances[i] = originalDistances[i] * quality;
        }
        
        UpdateAllLODGroups();
    }
    
    public void EnableAggressiveMode(bool enable)
    {
        if (enable)
        {
            ApplyAggressiveLOD();
        }
        else
        {
            RestoreNormalLOD();
        }
    }
    
    public int GetVisibleLODCount()
    {
        int visibleCount = 0;
        
        foreach (var lodGroup in lodGroups)
        {
            if (IsLODGroupVisible(lodGroup))
            {
                visibleCount++;
            }
        }
        
        return visibleCount;
    }
    
    bool IsLODGroupVisible(LODGroup lodGroup)
    {
        var lods = lodGroup.GetLODs();
        foreach (var lod in lods)
        {
            foreach (var renderer in lod.renderers)
            {
                if (renderer != null && renderer.enabled)
                {
                    return true;
                }
            }
        }
        return false;
    }
}

효율적인 렌더링 최적화는 앱인토스 게임의 핵심 성능 요소예요.
드로우콜 최소화, 적응적 LOD 시스템, 배치 렌더링을 통해 60FPS 목표를 달성하세요.