셰이더 API (surface.hlsl)

PackShade 함수 인터페이스, PackSurface 및 PackResult 구조체, 내장 헬퍼 함수와 환경 변수를 정리합니다.

셰이더 진입점 규격

모든 셰이더 팩의 surface.hlsl은 단 하나의 진입 함수를 구현해야 합니다:

PackResult PackShade(PackSurface s);

이 함수는 캐릭터 모델의 모든 재질 픽셀에 대해 실행되며, MMDX12의 기본 음영 계산 대신 호출됩니다.

컴파일 환경 및 주의점

  • 컴파일러: DirectX Shader Compiler(DXC)를 사용하며 셰이더 모델 6.0(레이 트레이싱 경로는 6.5)으로 빌드됩니다.
  • HLSL 표준: -HV 2018 (HLSL 2018 문법 규칙)을 따릅니다.
  • 헤더 포함 (#include): surface.hlsl 내부의 #include는 해당 파일이 위치한 팩 폴더를 기준으로 상대 경로로 해석됩니다.
  • 예약어 주의: DXC에서 line은 예약어이므로 변수명이나 함수명으로 사용하지 마세요.
  • 셰이더 캐시: 팩이 적용된 캐릭터가 화면에 처음 그려질 때 컴파일되며, 결과는 <MMDX12>/shader_cache에 보관됩니다. 팩 파일을 수정하면 캐시가 자동으로 무효화되고 재컴파일됩니다.

입력 구조체: PackSurface

엔진이 픽셀 셰이더에서 계산하여 넘겨주는 표면 정보입니다:

필드타입설명
worldPosfloat3월드 공간 위치 (MMD 단위, +Y축 위쪽)
Nfloat3월드 노멀 벡터 (뒷면 렌더링 시 시선 방향으로 뒤집힘)
Vfloat3표면에서 카메라(시선)를 향하는 단위 벡터
Lfloat3주 광원(태양광)을 향하는 단위 벡터
uvfloat2텍스처 좌표
pixelfloat2화면 픽셀 좌표 (SV_Position.xy)
viewZfloat뷰 공간 깊이값 (카메라로부터의 거리)
texfloat4베이스 텍스처 색상 (감마 공간, 재질 모르프 반영, 텍스처 없으면 흰색)
alphafloat재질 알파 x 텍스처 알파 (0.004 미만은 이미 엔진이 버림)
shadowfloat태양광 그림자 (0: 그림자 속 .. 1: 빛을 받음). 그림자를 받지 않는 재질은 항상 1
materialClassuintPACK_BODY(0), PACK_SKIN(1), PACK_FACE(2), PACK_EYE(3), PACK_HAIR(4), PACK_WEAPON(5)

머리 본 좌표계 (Head Bone Frame)

캐릭터 얼굴 셰이딩 시 코나 뺨의 노멀 굴곡 때문에 얼굴이 찌그러지는 현상을 방지하기 위해 머리 본의 회전 틀을 제공합니다:

필드타입설명
hasHeadbool머리 본(頭 또는 首)이 모델에 존재하는지 여부 (없으면 기본 월드 축)
headPosfloat3머리 본의 월드 공간 중심 좌표
headRightfloat3모델 머리의 오른쪽 방향 벡터 (+X, 단위 길이)
headUpfloat3모델 머리의 위쪽 방향 벡터 (+Y, 단위 길이)
headForwardfloat3모델 머리가 바라보는 정면 방향 벡터 (MMD 모델은 -Z를 향함)
headScalefloat월드 단위 대비 모델 표시 비율 (크기 스케일)

출력 구조체: PackResult

셰이더 팩이 픽셀의 최종 색상과 속성을 엔진에 반환하는 구조체입니다:

struct PackResult {
    float3 color;         // 선형(Linear) HDR 광도 (엔진 규칙에 따라 gSunIntensity를 곱해야 함)
    float alpha;          // 최종 알파 투명도
    float reflectivity;   // 0.0 .. 1.0 (화면 공간 반사 SSR 및 레이 트레이싱 반사 세기)
    bool noAo;            // true 설정 시 앰비언트 오클루전(SSAO/RTAO)과 반사가 꺼짐 (얼굴용)
};

내장 헬퍼 함수 (pack_api.hlsli)

  • float PackParam(uint i): 매니페스트에 등록된 i번째 (0~15) 파라미터의 현재 슬라이더 값을 반환합니다.
  • 재질 속성 읽기 (감마 공간, 재질 모르프 적용됨):
    • float4 PackDiffuse(): 디퓨즈 색상 및 알파
    • float3 PackAmbient(): 앰비언트 환경광 계수
    • float3 PackSpecular(): 스펙큘러 하이라이트 색상
    • float PackSpecularPower(): 하이라이트 지수 (광택도)
    • float PackEdgeReflectivity(): 외곽 반사도
  • MMD 기본 라이팅 연산:
    • float3 PackMmdLit(PackSurface s): 원작 MMD의 라이팅 모델 saturate(ambient + diffuse * lightColor) * tex.rgb를 계산합니다.
  • 스피어 맵 (Sphere Map):
    • uint PackSphereMode(): 모델의 스피어 맵 합성 모드 (0: 없음, 1: 곱하기 Multiply, 2: 더하기 Add).
    • float3 PackSampleSphere(float3 N): 노멀 벡터를 기준으로 스피어 텍스처를 샘플링합니다.
  • 툰 텍스처 (Toon Map):
    • bool PackHasToon(): 해당 재질에 MMD 툰 텍스처가 지정되어 있는지 여부.
    • float3 PackSampleToon(float v): 툰 텍스처를 수직 축 v로 샘플링합니다 (v=0 밝은 쪽, v=1 어두운 그림자 쪽). 툰이 없으면 흰색을 반환합니다.

엔진 환경 변수

pack_api.hlsli 상단에서 다음과 같은 씬 조명 변수들을 활용할 수 있습니다:

  • gLightColor (float3): 태양광 주 광원 색상
  • gSunIntensity (float): 광원 세기 계수 (최종 컬러에 반드시 곱해 주어야 주간/야간 조절이 작동합니다)
  • gRimColor (float3), gRimStrength (float): 전역 림 라이트 설정
  • float3 Hemisphere(float3 n): 상공(gSkyZenith)과 지면(gGroundColor) 사이의 반구형 환경광
  • float3 SrgbToLinear(float3 c): sRGB 색상을 선형 HDR 공간으로 변환

팩 텍스처 (API 2)

pack.json에 선언한 텍스처("textures", 매니페스트 참고)는 배열 순서대로 인덱스로 샘플링합니다.

  • float4 PackSampleTex(uint i, float2 uv): i번 텍스처를 선언된 주소 모드(wrap / clamp)로 샘플링합니다.
  • float4 PackSampleTexLevel(uint i, float2 uv, float lod): 밉 레벨을 직접 지정합니다 (램프·데이터 맵은 lod = 0). 픽셀 단계 밖, 예를 들어 PackEdge에서도 쓸 수 있습니다.
  • uint2 PackTexSize(uint i): 레벨 0 크기(픽셀). i가 범위를 벗어나면 0, 0.
  • uint PackTexCount(): 선언된 텍스처 수.

"srgb": true(기본값) 텍스처는 선형 값을 돌려주므로 SrgbToLinear 없이 그대로 쓰세요. s.tex와 PackMmdLit은 기존처럼 감마 공간입니다. "srgb": false 텍스처는 저장된 값을 그대로 돌려줍니다. 없는 텍스처와 범위를 벗어난 인덱스는 흰색을 돌려주므로, 사용자가 텍스처 폴더를 지정하지 않아도 팩은 렌더링됩니다.

float3 ramp = PackSampleTexLevel(0, float2(saturate(dot(s.N, s.L) * 0.5 + 0.5), 0.5), 0).rgb;  // clamp 램프, 선형
float4 lm   = PackSampleTex(1, s.uv);   // "srgb": false 라이트맵: 채널 값 그대로

윤곽선: PackEdge (선택)

윤곽선은 PackShade와 별개로 엔진의 엣지 패스가 그립니다. surface.hlsl 맨 위에 PACK_HAS_EDGE를 정의하고 PackEdge를 구현하면 재질별로 윤곽선 색과 굵기를 정할 수 있습니다.

#define PACK_HAS_EDGE 1

PackEdgeResult PackEdge(uint materialClass, float4 mmdEdgeColor, float mmdEdgeSize) {
    PackEdgeResult e;
    e.color = materialClass == PACK_HAIR ? float4(0.25, 0.12, 0.10, 1.0) : mmdEdgeColor;   // 감마 공간 RGBA
    e.widthScale = materialClass == PACK_FACE ? 0.5 : 1.0;   // MMD 굵기에 곱함, 0이면 윤곽선 없음
    return e;
}

mmdEdgeColor / mmdEdgeSize는 PMX 재질의 엣지 값입니다. PACK_HAS_EDGE가 없으면 윤곽선은 엔진 기본값 그대로입니다. 실제 #define PACK_HAS_EDGE 줄만 인식하며, 주석에 적힌 이름은 무시합니다.