렌더링 최적화
앱인토스 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 목표를 달성하세요.
