저자: 包小猩
들어가며
최근 스타일라이즈드 렌더링을 작업하다가 흥미로운 현상을 하나 발견했습니다. 오프라인 렌더링에서는 Ramp 라이트가 매우 흔하게 쓰이는 기법인데, 실시간 렌더링 쪽에서는 의외로 거의 언급이 없더군요. “UE Ramp Light”, “그라데이션 조명” 같은 키워드로 검색해도 관련 기술 글이 거의 나오지 않았습니다.
Ramp 라이트를 잘 쓰면 광원의 레이어링(층위) 표현이 크게 좋아질 수 있기 때문에, 이번 글에서는 Ramp 기반 그라데이션 컬러 라이트를 UE5에서 구현하는 방법을 정리해 보겠습니다.
Ramp 라이트란?
전통적인 오프라인 렌더링에서 라이팅 아티스트는 거리(distance)나 NoL 같은 값을 이용해, 커스텀 컬러 Ramp를 샘플링하여 물체 표면의 조명 색이 어떻게 그라데이션 될지 제어하곤 합니다. 이 방식은 제한된 광원 수로도 더 풍부한 색 변화와 광량 레이어를 만들 수 있습니다.
말보다 결과를 먼저 보죠.

한 개 라이트의 냉/난색 그라데이션


두 개 라이트

아트 요구사항 분석
본격적으로 손대기 전에, 우리가 구현하려는 기능을 먼저 명확히 합시다. 전통 TA 감성(?)대로 요구사항을 정리해 보면 아래와 같습니다.
핵심 기능 요구사항
기능 설명 상세 설명
| 거리 기반 Ramp 샘플링 | 광원에서 물체까지의 거리에 따라 Ramp를 좌→우로 샘플링. RampAtlas에서 ID 선택 지원(재사용성) |
| 실시간 프리뷰 | Ramp 커브를 편집하면 뷰포트의 라이트 효과가 실시간으로 갱신 |
| 노멀 영향 | NoL에 따른 샘플링 오프셋. 거리 샘플링과 혼합 가능(효과는 꽤 미묘함) |

거리 & 노멀 비율로 블렌딩된 컬러
위 그림처럼, 이 방식으로 개조한 포인트 라이트는 소수의 라이트만으로도 매우 풍부한 빛/그림자 비주얼을 만들 수 있습니다. 꽤 ‘와’ 합니다.
우선은 포인트 라이트만 구현합니다. 다른 라이트 타입도 수정 방식은 크게 다르지 않습니다.
UE5 라이팅 파이프라인 설명
수정에 들어가기 전에, UE5 라이팅 파이프라인이 어떻게 동작하는지 알아야 합니다. 그래야 어디에 Ramp 로직을 끼워 넣을지 감이 잡힙니다.
먼저 아래 표가 ‘살펴봐야 하고 수정해야 하는’ 파일들입니다
파일 경로 역할 수정 내용
| PointLightComponent.h/cpp | 포인트 라이트 컴포넌트, 라이트 속성 정의 | Ramp 관련 디테일 패널 속성 추가 |
| SceneManagement.h | 라이트 파라미터 구조체(렌더러로 전달) | FLightRenderParameters에 파라미터 추가 |
| LightComponent.cpp | 라이트 컴포넌트 베이스, 셰이더 파라미터 구성 | Ramp 텍스처 리소스를 셰이더로 전달 |
| LightData.ush | 셰이더 측 라이트 데이터 구조 | FDeferredLightData에 파라미터 추가 |
| DeferredLightingCommon.ush | 디퍼드 라이팅 핵심 셰이더 코드 | Ramp 샘플링 및 가짜 GI 계산 삽입 |
다음은 구체적인 데이터 플로우입니다
C++ Component (PointLightComponent)
↓ [GetLightShaderParameters]
C++ Render Parameters (FLightRenderParameters)
↓ [MakeShaderParameters]
C++ Shader Parameters (FLightShaderParameters)
↓ [SetShaderParameters]
Shader Uniforms (DeferredLightUniforms)
↓ [GetDeferredLightData]
Shader Data (FDeferredLightData)
↓ [GetDynamicLighting]
Shader Logic (DeferredLightingCommon.ush)
보시다시피 손대는 지점이 많습니다. UE를 개조할 때 가장 빡센 부분이죠. 다만 라이트 데이터가 흘러가는 경로를 한번 익히고 나면, 이후 수정은 훨씬 덜 괴롭습니다.
구체적인 수정 단계
이제 실전입니다. 순서대로 하나씩 정리해 보겠습니다.
1단계: PointLightComponent.h에 디테일 패널 속성 추가
파일: Engine/Source/Runtime/Engine/Classes/Components/PointLightComponent.h
위치: 클래스 정의 내부(원하는 위치)
아래 3개 속성을 추가합니다: 전달할 Ramp 아틀라스, 샘플링 ID, 노멀 영향 가중치.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light|Distance Color", meta=(DisplayName="Distance Color Atlas"))
TObjectPtr<UCurveLinearColorAtlas> DistanceColorAtlas;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light|Distance Color", meta=(DisplayName="Distance Curve Index", ClampMin="0"))
int32 DistanceCurveIndex;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light|Distance Color", meta=(DisplayName="Normal Influence", ClampMin="0.0", ClampMax="1.0", UIMin="0.0", UIMax="1.0"))
float NormalInfluence = 0.0f;
여기서 주의할 점은 원시 포인터가 아니라 TObjectPtr을 사용한다는 것입니다. UE5의 새로운 포인터 래퍼로, 오브젝트 생명주기 관리에 더 유리합니다.
2단계: PointLightComponent.cpp에서 파라미터 수집/전달
파일: Engine/Source/Runtime/Engine/Private/Components/PointLightComponent.cpp
위치: GetLightShaderParameters 함수
if (DistanceColorAtlas && DistanceColorAtlas->GetResource())
{
LightParameters.DistanceColorAtlasResource = DistanceColorAtlas->GetResource();
LightParameters.DistanceCurveIndex = (DistanceCurveIndex >= 0) ? DistanceCurveIndex : 0;
LightParameters.NormalInfluence = FMath::Clamp(NormalInfluence, 0.0f, 1.0f);
}
else
{
LightParameters.DistanceColorAtlasResource = nullptr;
LightParameters.DistanceCurveIndex = 0xFFFFFFFF;
LightParameters.NormalInfluence = 0.0f;
}
수정 내용 설명
- GetResource(): UCurveLinearColorAtlas에서 하위 FTextureResource*를 가져옵니다. 렌더 스레드가 접근 가능한 텍스처 리소스입니다.
- 0xFFFFFFFF: 셰이더에서 Ramp 활성 여부를 빠르게 판별하기 위한 특수한 무효 값(sentinel)로 씁니다.
여기서는 파라미터를 수집만 하고, 실제 텍스처 리소스 전달은 **LightComponent.cpp**에서 이뤄집니다.
3단계: SceneManagement.h에 렌더 파라미터 정의
파일: Engine/Source/Runtime/Engine/Public/SceneManagement.h
위치: FLightRenderParameters 구조체
FTextureResource* DistanceColorAtlasResource;
int32 DistanceCurveIndex;
float NormalInfluence;
FLightRenderParameters()
:
DistanceColorAtlasResource(nullptr),
DistanceCurveIndex(-1),
NormalInfluence(0.0f)
{
}
수정 내용 설명
FLightRenderParameters는 게임 스레드와 렌더 스레드 사이의 “브릿지” 역할을 합니다. 렌더 모듈로 전달되어 셰이더 파라미터 구성에 쓰입니다.
주의: 생성자에서 모든 필드를 반드시 초기화해야 합니다.
4단계: LightComponent.cpp에서 텍스처 리소스를 셰이더 파라미터로 전달
파일: Engine/Source/Runtime/Engine/Private/Components/LightComponent.cpp
위치: MakeShaderParameters 함수
if (DistanceColorAtlasResource)
{
FRHITexture* TextureRHI = DistanceColorAtlasResource->GetTextureRHI();
if (TextureRHI)
{
OutShaderParameters.DistanceColorAtlas = TextureRHI;
OutShaderParameters.DistanceCurveIndex = (DistanceCurveIndex >= 0) ? (uint32)DistanceCurveIndex : 0;
OutShaderParameters.NormalInfluence = FMath::Clamp(NormalInfluence, 0.0f, 1.0f);
}
else
{
OutShaderParameters.DistanceColorAtlas = nullptr;
OutShaderParameters.DistanceCurveIndex = 0xFFFFFFFF;
OutShaderParameters.NormalInfluence = 0.0f;
UE_LOG(LogLightComponent, Warning, TEXT("PointLight Ramp: DistanceColorAtlasResource exists but GetTextureRHI() returned null. Using fallback texture."));
}
}
else
{
OutShaderParameters.DistanceColorAtlas = nullptr;
OutShaderParameters.DistanceCurveIndex = 0xFFFFFFFF;
OutShaderParameters.NormalInfluence = 0.0f;
}
// Bao
수정 내용 설명
- GetTextureRHI(): RHI 레벨 텍스처 핸들을 가져옵니다. 실제로 셰이더가 사용하는 리소스입니다.
- 널 체크: 텍스처 리소스가 아직 초기화되지 않았을 수 있으므로(비동기 로드) 체크 후 안전한 fallback을 제공합니다.
- 로그 출력: 이상 상황을 빠르게 찾기 위한 디버깅 도움.
핵심: 여기까지가 게임 스레드 → 렌더 스레드로 넘어가는 마지막 단계이고, 이후부터는 셰이더 영역입니다.
5단계: LightData.ush에 셰이더 데이터 구조 정의
파일: Engine/Shaders/Private/LightData.ush
위치 1: FLightShaderParameters 구조체
Texture2D DistanceColorAtlas;
uint DistanceCurveIndex;
half NormalInfluence;
위치 2: FDeferredLightData 구조체
uint DistanceCurveIndex;
half NormalInfluence;
설명
- FLightShaderParameters: C++에서 셰이더로 전달되는 파라미터 구조체( SHADER_PARAMETER 매크로와 대응 )
- FDeferredLightData: 셰이더 내부에서 쓰는 데이터 구조. DeferredLightUniforms에서 초기화됩니다.
주의: 텍스처 자체는 FDeferredLightData에 넣지 않고, 전역 DeferredLightUniforms를 통해 접근합니다. 이렇게 하면 Forward 경로 호환 문제를 피할 수 있습니다.
6단계: LightDataUniforms.ush에서 파라미터 초기화
파일: Engine/Shaders/Private/LightDataUniforms.ush
위치: GetDeferredLightData 함수(오버로드 2개 모두 수정)
Out.DistanceCurveIndex = DeferredLightUniforms.DistanceCurveIndex;
Out.NormalInfluence = DeferredLightUniforms.NormalInfluence;
전역 Uniform Buffer에서 값을 읽어 FDeferredLightData에 채우는 단계입니다.
7단계: DeferredLightingCommon.ush에서 Ramp 샘플링 로직 구현
파일: Engine/Shaders/Private/DeferredLightingCommon.ush
위치: GetDynamicLighting 함수, 라이트 감쇠 계산 이후
#if !RAYHITGROUPSHADER && !RAYGENSHADER && !RAYMISSSHADER && !RAYCALLABLESHADER
BRANCH
if (LightData.DistanceCurveIndex != 0xFFFFFFFF)
{
float Distance = length(ToLight);
float NormalizedDist = saturate(Distance * LightData.InvRadius); // 0~1
float NoL = dot(GBuffer.WorldNormal, L);
float NormalizedNoL = saturate(1.0f - NoL);
float RampSampleCoord = lerp(NormalizedDist, NormalizedNoL, LightData.NormalInfluence);
uint AtlasWidth, AtlasHeight;
DeferredLightUniforms.DistanceColorAtlas.GetDimensions(AtlasWidth, AtlasHeight);
float2 RampUV;
RampUV.x = RampSampleCoord;
RampUV.y = (float(LightData.DistanceCurveIndex) + 0.5f) / float(AtlasHeight);
float3 RampColor = Texture2DSampleLevel(DeferredLightUniforms.DistanceColorAtlas, GlobalBilinearClampedSampler, RampUV, 0).rgb;
MaskedLightColor *= RampColor * LightMask;
}
#endif
여기가 Ramp 라이트 효과의 핵심입니다. 기본 광원의 “색”만 변경합니다. (그림자 영역 처리 쪽은 다음 단계에서 다룹니다.)
주의할 점
구현 과정에서 크래시와 컴파일 에러를 정말 많이 밟았는데, 그중 핵심 포인트를 정리해 둡니다.
1) 널 포인터 체크 - 크래시 방지의 핵심
문제: Ramp 커브를 빠르게 드래그할 때 자주 크래시. 콜스택은 UpdateTexture에서 널 접근.
원인
- 텍스처 리소스가 아직 초기화되지 않았을 수 있음(비동기 로드)
- 빠른 드래그로 콜백이 연속 발생해 데이터 레이스 발생
- 오브젝트가 GC로 파괴 중일 수 있음
해결: CurveLinearColorAtlas.cpp의 UpdateTexture에 3중 방어 코드를 추가
static void UpdateTexture(UCurveLinearColorAtlas& Atlas)
{
if (Atlas.SrcData.Num() == 0)
{
UE_LOG(LogTexture, Warning, TEXT("UpdateTexture: SrcData is empty, skipping update"));
return;
}
const int32 TextureDataSize = Atlas.Source.CalcMipSize(0);
if (TextureDataSize <= 0 || TextureDataSize != Atlas.SrcData.Num() * sizeof(FFloat16Color))
{
UE_LOG(LogTexture, Warning, TEXT("UpdateTexture: TextureDataSize mismatch, expected %d, got %d"),
Atlas.SrcData.Num() * sizeof(FFloat16Color), TextureDataSize);
return;
}
uint32* TextureData = reinterpret_cast<uint32*>(Atlas.Source.LockMip(0));
if (TextureData == nullptr)
{
UE_LOG(LogTexture, Warning, TEXT("UpdateTexture: Failed to lock mip, texture resource may not be initialized"));
return;
}
FMemory::Memcpy(TextureData, Atlas.SrcData.GetData(), TextureDataSize);
Atlas.Source.UnlockMip(0);
}
2) UE 텍스처 시스템 팁 - 삽질에서 얻은 메모
Ramp 라이트 구현하면서 UE의 텍스처 시스템을 대략 훑어봤는데, 메모 겸 공유합니다.
팁 1: 텍스처 리소스의 3층 구조
UTexture (게임 스레드 오브젝트)
↓ GetResource()
FTextureResource (렌더 스레드 리소스)
↓ GetTextureRHI()
FRHITexture (RHI 레벨 핸들)
예시 코드
// 잘못된 예 ❌
OutShaderParameters.Texture = MyTexture; // UTexture는 셰이더로 직접 전달 불가
// 올바른 예 ✅
if (MyTexture && MyTexture->GetResource())
{
FRHITexture* TextureRHI = MyTexture->GetResource()->GetTextureRHI();
if (TextureRHI)
{
OutShaderParameters.Texture = TextureRHI;
}
}
팁 2: 비동기 로딩 리소스의 결손 처리
if (TextureRHI)
{
// 사용자 지정 텍스처 사용
OutShaderParameters.Texture = TextureRHI;
}
else
{
// fallback 텍스처(화이트 1x1)
OutShaderParameters.Texture = GWhiteTexture->TextureRHI;
OutShaderParameters.Index = 0xFFFFFFFF; // 무효 마킹
}
팁 3: 샘플러 선택
// 잘못된 예 ❌
float3 Color = DeferredLightUniforms.DistanceColorAtlas.Sample(SamplerState, UV).rgb;
// 올바른 예 ✅
float3 Color = Texture2DSampleLevel(
DeferredLightUniforms.DistanceColorAtlas,
GlobalBilinearClampedSampler, // Bilinear + Clamp
UV,
0 // Mip Level 0
).rgb;
왜 Bilinear + Clamp?
- Bilinear: 부드러운 전이(색 점프 방지)
- Clamp: 가장자리 반복 방지(다른 행을 잘못 샘플링하는 문제 방지)
- Level 0: Mipmap을 쓰지 않아 정확도 유지
3) 스레드 세이프티 - 게임 스레드 vs 렌더 스레드
UE는 멀티스레드 구조이고 게임 스레드/렌더 스레드가 병렬로 돌아갑니다. 스레드 세이프티를 놓치면 기능을 붙인 직후부터 사용 중 크래시가 빈번해집니다.
문제 시나리오
게임 스레드: 사용자가 Ramp 커브 드래그
↓
OnCurveUpdated() 호출
↓
UpdateTexture()로 텍스처 데이터 변경
↓
(동시에) 렌더 스레드가 텍스처 데이터 읽는 중
↓
크래시! 데이터 레이스
해결: 렌더 커맨드 사용
// 잘못된 예 ❌
void OnCurveUpdated()
{
// 텍스처 데이터를 즉시 변경(위험)
Atlas.Source.LockMip(0);
// ...
}
// 올바른 예 ✅
void OnCurveUpdated()
{
// 텍스처 업데이트를 렌더 스레드로 위임
UpdateTexture(*this); // 내부에서 UpdateResource() 사용
// UpdateResource()는 업데이트 커맨드를 렌더 큐에 넣고
// 렌더 스레드가 안전한 타이밍에 적용
}
핵심
- FlushRenderingCommands()는 게임 스레드를 막아서 끊김이 생기므로 피합니다.
- UpdateResource()로 비동기 업데이트.
- 오브젝트 유효성 체크를 추가해 파괴 중 오브젝트 접근을 방지.
실시간 갱신(리프레시) 메커니즘 상세
실시간 프리뷰는 이 시스템의 핵심입니다. 사용자가 Ramp 커브를 편집할 때, 뷰포트의 라이트 효과가 즉시 갱신되어야 합니다.
갱신 플로우
sequenceDiagram
participant User as 사용자
participant CurveEditor as 커브 에디터
participant Atlas as CurveLinearColorAtlas
participant Component as PointLightComponent
participant Viewport as 뷰포트
User->>CurveEditor: 키/색상 드래그
CurveEditor->>Atlas: OnCurveUpdated()
Atlas->>Atlas: UpdateTexture() - 텍스처 업데이트
Atlas->>Component: FCoreUObjectDelegates::OnObjectPropertyChanged
Component->>Component: OnDistanceColorAtlasChanged()
Component->>Component: MarkRenderStateDirty()
Component->>Viewport: FEditorSupportDelegates::RedrawAllViewports
Viewport->>User: 결과 갱신
수정 코드 - 커브 변경 감지
파일: CurveLinearColorAtlas.cpp
void UCurveLinearColorAtlas::OnCurveUpdated(UCurveBase* Curve, EPropertyChangeType::Type ChangeType)
{
if (!IsValid(Curve) || !IsValid(this) ||
Curve->HasAnyFlags(RF_BeginDestroyed) || HasAnyFlags(RF_BeginDestroyed) ||
Curve->IsUnreachable() || IsUnreachable())
{
UE_LOG(LogTexture, Warning, TEXT("OnCurveUpdated: Object is invalid or being destroyed, skipping update"));
return;
}
UCurveLinearColor* Gradient = CastChecked<UCurveLinearColor>(Curve);
int32 SlotIndex = GradientCurves.Find(Gradient);
if (SlotIndex != INDEX_NONE && (uint32)SlotIndex < MaxSlotsPerTexture())
{
int32 StartXY = SlotIndex * TextureSize;
RenderGradient(SrcData, Gradient, StartXY, SizeXY, bDisableAllAdjustments);
AsyncTask(ENamedThreads::GameThread, [this, Curve, ChangeType]()
{
if (!IsValid(this) || !IsValid(Curve)) { return; }
FTextureCompilingManager::Get().FinishCompilation({this});
UpdateTexture(*this);
FPropertyChangedEvent PropertyEvent(nullptr, ChangeType);
FCoreUObjectDelegates::OnObjectPropertyChanged.Broadcast(Curve, PropertyEvent);
});
// Bao
}
// Bao
}
오브젝트 유효성 체크를 통해 GC 회수/빠른 드래그 상황에서 파괴 중 오브젝트 접근을 막습니다. 또한 텍스처 업데이트는 비동기로 처리해 게임 스레드 블로킹을 피하고, 마지막에 이벤트를 수동 브로드캐스트하여(라이트 컴포넌트 포함) 모든 리스너에게 변화를 알립니다.
핵심 코드 2: 라이트 컴포넌트가 변경에 반응
파일: PointLightComponent.cpp
void UPointLightComponent::OnDistanceColorAtlasChanged(UObject* InObject, FPropertyChangedEvent& InPropertyChangedEvent)
{
if (InObject == DistanceColorAtlas)
{
// Atlas 자체가 변경됨(예: 텍스처 크기 변경)
MarkRenderStateDirty();
#if WITH_EDITOR
// 디바운스: 과도한 리프레시로 인한 성능 문제 방지
double CurrentTime = FPlatformTime::Seconds();
if (CurrentTime - LastViewportRefreshTime >= MinViewportRefreshInterval)
{
FEditorSupportDelegates::RedrawAllViewports.Broadcast();
LastViewportRefreshTime = CurrentTime;
}
#endif
}
else if (DistanceColorAtlas)
{
// Atlas 내부의 특정 커브 변경 여부 확인
for (UCurveLinearColor* Curve : DistanceColorAtlas->GradientCurves)
{
if (Curve && Cast<UCurveLinearColor>(InObject) == Curve)
{
MarkRenderStateDirty();
#if WITH_EDITOR
double CurrentTime = FPlatformTime::Seconds();
if (CurrentTime - LastViewportRefreshTime >= MinViewportRefreshInterval)
{
FEditorSupportDelegates::RedrawAllViewports.Broadcast();
LastViewportRefreshTime = CurrentTime;
}
#endif
break;
}
}
}
}
MarkRenderStateDirty()로 다음 프레임에 렌더 상태를 갱신하도록 표시하고, RedrawAllViewports()로 뷰포트를 즉시 갱신해 실시간 프리뷰를 구현합니다. 또한 30fps 수준으로 리프레시를 제한해 드래그 중 과도한 갱신으로 인한 끊김을 줄입니다.
리스너 등록/해제
파일: PointLightComponent.cpp
void UPointLightComponent::OnRegister()
{
Super::OnRegister();
#if WITH_EDITOR
if (DistanceColorAtlas)
{
FCoreUObjectDelegates::OnObjectPropertyChanged.AddUObject(this, &UPointLightComponent::OnDistanceColorAtlasChanged);
}
#endif
}
void UPointLightComponent::BeginDestroy()
{
#if WITH_EDITOR
// 메모리 누수 방지
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this);
#endif
Super::BeginDestroy();
}
보충: 추가 크래시 대응
라이팅과 얽힌 코드가 많아서(특히 볼륨 렌더링 주변) 크래시가 자주 발생할 수 있습니다. 아래 4개 파일을 수정해야 합니다.

위 네 파일에서 아래 코드 영역( FDeferredLightUniformStruct DeferredLightUniform; 를 검색하면 쉽게 찾습니다)을 찾아, 굵게 표시된 부분처럼 else 경로에서 안전한 값으로 초기화해 주세요.
FDeferredLightUniformStruct DeferredLightUniform;
if (bApplyDirectLighting && (LightSceneInfo != nullptr))
{
DeferredLightUniform = GetDeferredLightParameters(View, *LightSceneInfo);
}
else
{
FMemory::Memzero(&DeferredLightUniform, sizeof(FDeferredLightUniformStruct));
if (GSystemTextures.WhiteDummy && GSystemTextures.WhiteDummy->GetRHI())
{
DeferredLightUniform.LightParameters.DistanceColorAtlas = GSystemTextures.WhiteDummy->GetRHI();
}
else if (GWhiteTexture && GWhiteTexture->TextureRHI)
{
DeferredLightUniform.LightParameters.DistanceColorAtlas = GWhiteTexture->TextureRHI.GetReference();
}
else if (GBlackTexture && GBlackTexture->TextureRHI)
{
DeferredLightUniform.LightParameters.DistanceColorAtlas = GBlackTexture->TextureRHI.GetReference();
}
DeferredLightUniform.LightParameters.DistanceCurveIndex = 0xFFFFFFFF;
}
이 처리는 위 if가 실패했을 때 DeferredLightUniform이 초기화되지 않은 상태로 GPU에 전달되는 것을 막기 위한 것입니다. 우리가 추가한 값이 비어 있는 상태로 접근되면 GPU 크래시로 이어질 수 있으므로, 반드시 안전한 기본값을 채워 주세요.
이상으로 수정은 끝입니다. 도움이 되었길 바랍니다. 다음에 또 봅시다.
원문
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [번역] 넷이즈 레이훠 LGDC 시리즈|게임 개발에서 ‘마법’처럼 쓰이는 기술(상): 날씨 시스템 설계와 실전 (0) | 2026.05.24 |
|---|---|
| 이 블로그를 유익하게 읽는 방법 (8) | 2026.05.24 |
| [번역] 넷이즈 레이훠(LGDC) 시리즈|모바일 파이프라인 상수 소개 및 구현 공유 (0) | 2026.05.24 |
| [번역] UE5에서 성능 좋은 서브서피스 옥(翡翠) 표현하기 (0) | 2026.05.24 |
| [번역] UE5 InstancedSkinnedMeshComponent 기술 심층 분석 (0) | 2026.05.20 |