Base Color sRGB·Linear 설정 오류를 일괄 검출하는 텍스처 임포트 검증 파이프라인

Base Color가 sRGB로 임포트되지 않아 발생하는 명암 오류를 진단하고 Unity AssetPostprocessor와 규칙 기반 스캔으로 텍스처 임포트 설정을 일괄 검증하는 방법을 정리합니다.

✓ 복사 완료!
챕터머티리얼 & 텍스처
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
Base Color sRGB·Linear 설정 오류를 일괄 검출하는 텍스처 임포트 검증 파이프라인

핵심 요약

Base Color와 Albedo처럼 사람이 보는 색을 담은 텍스처는 sRGB 텍스처로 저장·샘플링한 뒤 셰이더에서 Linear 값으로 변환해 조명 계산에 사용해야 합니다. 반대로 Normal, Roughness, Metallic, AO, 마스크처럼 수치 자체가 의미인 텍스처는 Linear 또는 Raw로 취급해야 합니다.

검증 파이프라인은 픽셀만 보고 의미를 추측하지 않고 머티리얼 슬롯 → 텍스처 의미 → 임포터 설정을 연결해 검사합니다. Unity의 TextureImporter.sRGBTexture는 프로젝트 색 공간이 Linear일 때 샘플링 중 sRGB→Linear 변환 여부를 결정합니다. 이 설정을 기준으로 Base Color는 보통 true, 데이터 텍스처는 false인지 일괄 검증하고 CI에서 오류를 차단합니다.

Base Color의 sRGB와 Linear는 무엇이 다른가?

sRGB 텍스처는 왜 필요한가?

디지털 이미지 파일의 RGB 값은 일반적으로 디스플레이와 저장 효율을 위해 sRGB 인코딩을 사용합니다. sRGB의 값 0.5는 물리적인 광량의 50%가 아니라 대략 21.4%의 선형 광량에 해당합니다. 따라서 색상 텍스처를 조명 계산에 바로 사용하면 안 됩니다.

셰이더는 다음과 같이 sRGB 값을 선형 값으로 변환한 뒤 조명 계산을 수행해야 합니다.

Clinear={CsRGB/12.92CsRGB≤0.04045((CsRGB+0.055)/1.055)2.4CsRGB>0.04045C_{linear} = \begin{cases} C_{sRGB} / 12.92 & C_{sRGB} \le 0.04045 \\ ((C_{sRGB} + 0.055) / 1.055)^{2.4} & C_{sRGB} > 0.04045 \end{cases}

엔진의 텍스처 임포터에서 sRGB 옵션을 켜면 그래픽 API의 sRGB 리소스 또는 셰이더 샘플링 경로를 통해 이 변환이 적용됩니다. 엔진과 렌더 파이프라인에 따라 실제 구현 방식은 다를 수 있지만 핵심 원칙은 동일합니다. 색상 데이터는 조명 계산 전에 Linear가 되어야 합니다.

텍스처 의미저장·샘플링 기준대표 임포트 설정잘못 설정했을 때
Base Color, Albedo, LDR Emissive 색상sRGB 입력 후 셰이더에서 Linear 사용sRGB=true색이 지나치게 어둡거나 밝고 채도가 달라짐
Normal MapLinear 수치 데이터sRGB 해제노멀 방향이 왜곡되어 표면 조명이 틀어짐
Roughness, Metallic, AOLinear 수치 데이터sRGB 해제거칠기·금속성·차폐 값이 비선형으로 변함
마스크, ID, HeightLinear 수치 데이터sRGB 해제임계값과 블렌딩 결과가 달라짐
UI 아이콘·색상 그레이디언트사용 경로에 따라 결정렌더 경로 확인UI 색상 또는 합성 결과가 변함

HDR 텍스처는 일반 색상 텍스처와 달리 선형 값으로 저장하는 경우가 많으므로, 특히 HDR Emissive 텍스처는 임포터 기본값을 적용하지 말고 파일 형식과 셰이더 입력 계약을 확인합니다.

Base Color를 Linear로 임포트하면 왜 문제가 발생하는가?

Base Color를 Linear 또는 Raw로 임포트하면 파일 안의 sRGB 코드값이 이미 선형 값인 것처럼 사용됩니다. 예를 들어 파일의 중간 회색 0.5를 선형 0.5로 처리하면 실제 sRGB 중간 회색보다 훨씬 많은 광량으로 계산됩니다. 이후 톤 매핑과 조명 환경에 따라 밝기 차이가 달라지므로 색상 관리 전체에서 불일치가 발생한다고 보는 편이 정확합니다.

반대로 Base Color에 sRGB 변환을 중복 적용하면 이미 선형으로 변환된 값을 다시 변환하는 경로가 생길 수 있습니다. 따라서 검증 대상은 텍스처 파일 하나가 아니라 다음 세 가지의 계약입니다.

  1. 아티스트가 저장한 색 공간
  2. 텍스처 임포터의 색상 데이터 플래그
  3. 셰이더 프로퍼티가 기대하는 입력 공간

Base Color의 sRGB 입력과 Linear 조명 계산 변환을 설명하는 합성 개념 이미지로, 실제 Unity 캡처나 측정 데이터가 아닙니다

텍스처 임포트 검증은 어떻게 설계해야 하는가?

픽셀의 분포만 분석해 Base Color인지 판단하는 방식은 신뢰하기 어렵습니다. 검은 마스크도 존재할 수 있고 회색 Base Color도 존재할 수 있기 때문입니다. 따라서 의미를 판정하는 우선순위를 프로젝트 규칙으로 고정해야 합니다.

우선순위판정 정보예시목적
1머티리얼 슬롯·셰이더 프로퍼티_BaseMap, Base Color, Albedo실제 사용 의미 확인
2명시적 메타데이터.texturemeta.json, export 태그예외와 커스텀 셰이더 지원
3폴더 규칙Textures/BaseColor/, Textures/Mask/머티리얼 연결 전 사전 검증
4파일명 규칙_BC, _BaseColor, _R, _M빠른 누락 검출
5픽셀 분석채널 수, 평균값, 색상 분산보조 경고에만 사용

권장 규칙은 BaseColor와 Albedo를 색상 데이터로 명시하고 Roughness, Metallic, AO, Normal, Mask, ORM을 수치 데이터로 명시하는 것입니다. ORM처럼 여러 채널을 한 파일에 패킹한 텍스처는 파일 전체를 Linear로 취급해야 합니다. 채널마다 sRGB를 선택하는 것이 아니라 셰이더에서 각 채널을 수치 데이터로 읽어야 합니다.

검증 결과는 어떤 상태로 나누는가?

상태의미CI 처리
Pass의미와 임포터 설정이 일치통과
ErrorBase Color인데 sRGB=false 등 명백한 불일치빌드 차단
Warning이름과 머티리얼 사용 정보가 충돌함검토 후 허용
Unknown의미를 결정할 규칙이 없음신규 에셋은 차단 권장
Suppressed승인된 예외 목록에 등록됨사유와 만료일 기록

Unity에서 Base Color sRGB 오류를 일괄 검출하는 방법

아래 예시는 Unity Editor의 AssetPostprocessor와 메뉴 기반 전체 스캔에 사용할 수 있는 최소 구조입니다. Editor API를 쓰는 스크립트는 Editor 폴더나 Editor 전용 어셈블리에 두고, 각 코드 블록은 별도 파일로 봅니다. 실제 프로젝트에서는 공통 규칙을 각 검사기에 연결하고 셰이더에 맞춰 Base Color 프로퍼티 목록과 데이터 텍스처 규칙을 조정해야 합니다.

1단계: 프로젝트 규칙을 코드로 고정한다

_BaseMap과 _MainTex를 색상 슬롯으로 보고 _BumpMap, _MetallicGlossMap을 데이터 슬롯으로 분류하는 예시입니다.

#if UNITY_EDITOR
using System.Collections.Generic;

static class TextureImportRules
{
    public static readonly HashSet<string> ColorProperties = new HashSet<string>()
    {
        "_BaseMap",
        "_MainTex",
        "_BaseColorMap",
        "_Albedo"
    };

    public static readonly HashSet<string> DataProperties = new HashSet<string>()
    {
        "_BumpMap",
        "_NormalMap",
        "_MetallicGlossMap",
        "_OcclusionMap",
        "_MaskMap"
    };
}
#endif

_BaseColor처럼 Color 타입인 머티리얼 색상값과 텍스처 프로퍼티는 구분해야 합니다. 머티리얼의 색상 틴트 값은 텍스처의 sRGB 플래그를 대신하지 않습니다.

2단계: 임포트 시점에 즉시 검사한다

OnPreprocessTexture는 텍스처가 임포트될 때 실행되므로 잘못된 설정을 조기에 발견할 수 있습니다. 아래 코드는 폴더와 파일명 규칙으로 기본 의미를 판정하고 설정이 다르면 오류를 출력합니다.

#if UNITY_EDITOR
using System.IO;
using UnityEditor;

class TextureImportValidator : AssetPostprocessor
{
    void OnPreprocessTexture()
    {
        var importer = (TextureImporter)assetImporter;
        var path = assetPath.ToLowerInvariant();
        var fileName = Path.GetFileNameWithoutExtension(path);

        bool looksLikeColor = path.Contains("/basecolor/") ||
                              path.Contains("/albedo/") ||
                              fileName.EndsWith("_bc") ||
                              fileName.EndsWith("_basecolor") ||
                              fileName.EndsWith("_albedo");

        bool looksLikeData = path.Contains("/normal/") ||
                             path.Contains("/mask/") ||
                             path.Contains("/roughness/") ||
                             path.Contains("/metallic/") ||
                             fileName.EndsWith("_normal") ||
                             fileName.EndsWith("_roughness") ||
                             fileName.EndsWith("_metallic") ||
                             fileName.EndsWith("_mask") ||
                             fileName.EndsWith("_r") ||
                             fileName.EndsWith("_m") ||
                             fileName.EndsWith("_orm");

        if (looksLikeColor && !importer.sRGBTexture)
            UnityEngine.Debug.LogError($"Base Color must use sRGB import: {assetPath}");

        if (looksLikeData && importer.sRGBTexture)
            UnityEngine.Debug.LogError($"Data texture must use Linear import: {assetPath}");
    }
}
#endif

이 단계는 새 텍스처가 들어올 때 즉시 알려 주는 데 적합합니다. 기존 에셋 전체를 검사하지 않으므로 저장소 기준의 일괄 검증은 별도 메뉴나 CI 명령으로 구현해야 합니다.

3단계: 저장소 전체를 스캔하고 CI에서 실패시킨다

전체 스캔에서는 AssetDatabase.FindAssets("t:Texture2D")로 텍스처를 찾고 각 임포터 설정을 확인합니다. 실제 Base Color 사용 여부까지 검사하려면 머티리얼을 순회해 텍스처 프로퍼티와 연결된 텍스처 경로를 인덱싱해야 합니다.

#if UNITY_EDITOR
using System.IO;
using UnityEditor.Build;
using UnityEditor;
using UnityEngine;

public static class TextureImportBatchValidator
{
    [MenuItem("Tools/Art Tech/Validate Texture Color Space")]
    public static void ValidateAll()
    {
        int errors = 0;
        string[] guids = AssetDatabase.FindAssets("t:Texture2D");

        foreach (string guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            var importer = AssetImporter.GetAtPath(path) as TextureImporter;
            if (importer == null) continue;

            string normalized = path.ToLowerInvariant();
            string fileName = Path.GetFileNameWithoutExtension(normalized);
            bool isBaseColor = normalized.Contains("/basecolor/") ||
                               normalized.Contains("/albedo/") ||
                               fileName.EndsWith("_bc") ||
                               fileName.EndsWith("_basecolor") ||
                               fileName.EndsWith("_albedo");
            bool isData = normalized.Contains("/normal/") ||
                          normalized.Contains("/roughness/") ||
                          normalized.Contains("/metallic/") ||
                          normalized.Contains("/mask/") ||
                          fileName.EndsWith("_normal") ||
                          fileName.EndsWith("_roughness") ||
                          fileName.EndsWith("_metallic") ||
                          fileName.EndsWith("_mask") ||
                          fileName.EndsWith("_r") ||
                          fileName.EndsWith("_m") ||
                          fileName.EndsWith("_orm");

            if (isBaseColor && !importer.sRGBTexture)
            {
                Debug.LogError($"[TextureImport] Base Color is Linear: {path}");
                errors++;
            }

            if (isData && importer.sRGBTexture)
            {
                Debug.LogError($"[TextureImport] Data texture is sRGB: {path}");
                errors++;
            }
        }

        if (errors > 0)
            throw new BuildFailedException($"Texture import validation failed: {errors} error(s).");

        Debug.Log("Texture import validation passed.");
    }
}
#endif

실무에서는 검사 결과를 JSON으로 내보내고 errorCount, warningCount, unknownCount, assetPath, expectedSrgb, actualSrgb, reason을 기록하면 CI 로그와 아트 리뷰 도구에서 재사용하기 쉽습니다.

머티리얼 슬롯까지 추적하는 검증 파이프라인은 어떻게 구현할까?

파일명 규칙만으로는 같은 텍스처가 여러 머티리얼에서 서로 다른 의미로 사용되는 문제를 해결하기 어렵습니다. 더 강한 검증은 다음 순서로 참조 그래프를 만듭니다.

flowchart TD
    A[텍스처 임포트] --> B[파일명·폴더·메타데이터 판정]
    B --> C[머티리얼과 셰이더 프로퍼티 인덱싱]
    C --> D{기대 색 공간과 importer.sRGBTexture 비교}
    D -->|일치| E[Pass 리포트]
    D -->|불일치| F[Error 리포트 및 CI 실패]
    D -->|정보 부족| G[Unknown 또는 Warning]
    G --> H[예외 목록과 승인 만료일 확인]

Unity 머티리얼을 순회할 때는 셰이더가 제공하는 프로퍼티 중 텍스처 타입만 읽습니다. Material.GetTexture(propertyName)으로 텍스처를 얻고 AssetDatabase.GetAssetPath(texture)로 경로를 역추적하면 특정 텍스처가 어느 슬롯에서 사용되는지 확인할 수 있습니다.

머티리얼 슬롯기대 설정검사 이유
_BaseMap, _MainTex의 색상 용도sRGBTexture=true색상 텍스처를 Linear 셰이더 입력으로 변환
_BumpMap, _NormalMapsRGBTexture=false접선 공간 벡터 데이터 보존
_MetallicGlossMap, _MaskMapsRGBTexture=false패킹된 채널 값을 수치로 사용
커스텀 셰이더의 Texture2D 입력셰이더 계약에 따름프로퍼티 이름만으로 의미 확정 불가

하나의 텍스처를 Base Color와 마스크 슬롯에 동시에 연결했다면 자동 수정해서는 안 됩니다. 먼저 텍스처를 분리하거나 셰이더 계약을 명확히 해야 합니다. 자동 수정은 기대값과 실제값이 모두 확실한 경우에만 허용하는 것이 안전합니다.

자동 수정은 어디까지 허용해야 하는가?

오류가 확실한 규칙에서는 importer.sRGBTexture를 자동으로 설정할 수 있습니다. 그러나 임포트 설정을 바꾸면 재임포트와 아티스트의 로컬 캐시 변화가 발생하므로 변경 사유를 로그와 리포트에 남겨야 합니다.

권장 정책은 다음과 같습니다.

  1. 신규 텍스처는 의미 메타데이터 또는 명명 규칙이 없으면 Unknown으로 표시합니다.
  2. Base Color와 데이터 텍스처의 설정 불일치는 Error로 처리합니다.
  3. 확실한 단일 규칙에만 자동 수정 모드를 제공합니다.
  4. 머티리얼 슬롯 충돌은 자동 수정하지 않고 수동 검토로 보냅니다.
  5. 예외에는 담당자, 사유, 등록일, 만료일을 기록합니다.
  6. CI에서는 오류가 하나라도 있으면 아트 콘텐츠 검증 단계를 실패시킵니다.

자동 수정 기능을 만들더라도 먼저 dry-run 또는 리포트 전용 모드를 제공하는 편이 좋습니다. 대규모 프로젝트에서는 설정 변경보다 잘못된 의미 분류가 더 큰 문제를 만들 수 있기 때문입니다.

Pass·Error·Unknown 수치를 포함한 합성 텍스처 검증 리포트 이미지이며 실제 프로젝트 측정 결과가 아닙니다

검증 결과를 CI에 연결하는 방법

CI는 아티스트가 사용하는 에디터와 같은 판정 규칙을 사용해야 합니다. 로컬에서는 메뉴 명령으로 실행하고 CI에서는 배치 모드의 Unity 편집기에서 같은 정적 메서드를 호출하는 방식이 일반적입니다.

검사 결과의 계약은 다음처럼 정의할 수 있습니다.

{
  "errorCount": 1,
  "warningCount": 0,
  "unknownCount": 0,
  "failOnWarning": false,
  "issues": [
    {
      "assetPath": "Assets/Art/Textures/Robot_BaseColor.png",
      "expectedSrgb": true,
      "actualSrgb": false,
      "reason": "BaseColor folder rule"
    }
  ]
}

CI 단계에서는 다음 검사를 분리하면 원인을 찾기 쉽습니다.

  • 임포터 설정 검증: sRGBTexture, 텍스처 타입, 압축 형식, 알파 설정 검사
  • 참조 검증: 머티리얼 슬롯과 셰이더 프로퍼티의 의미 검사
  • 시각 검증: 대표 조명 환경에서 렌더링 스냅샷 비교
  • 승인 검증: 예외 파일의 담당자와 만료일 검사

시각 검증은 색 공간 오류를 보완하지만 대체하지는 않습니다. 조명, 노출, 톤 매핑, 색 보정이 바뀌면 스냅샷의 픽셀 차이가 달라질 수 있으므로 원인에 가까운 임포터 플래그 검사를 먼저 실행해야 합니다.

자주 묻는 질문 (FAQ)

Base Color는 항상 sRGB로 설정해야 하나요?

일반적인 디퓨즈·알베도·Base Color 텍스처라면 그렇습니다. 다만 셰이더가 색상 텍스처를 데이터로 의도적으로 사용하는 특수한 경우에는 셰이더 입력 계약을 기준으로 결정해야 합니다.

ORM 텍스처는 채널 중 하나가 색상처럼 보여도 sRGB를 켜면 안 되나요?

켜면 안 됩니다. Occlusion, Roughness, Metallic은 모두 수치 데이터이며 패킹된 ORM 파일 전체를 Linear로 읽어야 합니다. 색상이 보인다는 시각적 인상은 색 공간 판정 기준이 아닙니다.

임포터 옵션만 검사하면 충분한가요?

충분하지 않습니다. 같은 텍스처가 어떤 머티리얼 슬롯과 셰이더에서 사용되는지 확인해야 합니다. 임포터 설정과 셰이더의 기대 입력이 일치하는지까지 검사해야 재사용 에셋과 커스텀 셰이더의 오류를 줄일 수 있습니다.

정리

Base Color의 sRGB·Linear 오류는 텍스처 옵션 하나의 실수가 아니라 색상 데이터와 수치 데이터의 의미가 파이프라인에서 끊어진 문제입니다. 머티리얼 슬롯 또는 메타데이터로 의미 판정 → 기대 색 공간 정의 → 임포터 설정 비교 → 리포트와 CI 실패 처리의 순서를 고정하면 대규모 텍스처 저장소에서도 오류를 일관되게 검출할 수 있습니다.

가장 먼저 적용할 최소 규칙은 간단합니다. Base Color·Albedo는 sRGB=true, Normal·Roughness·Metallic·AO·Mask·ORM은 sRGB=false로 검사하고 예외와 커스텀 셰이더는 명시적인 계약으로 관리하세요.