- 하늘 배경용 달과 가스 행성은 실제 구 메시 없이도 원형 디스크와 픽셀 셰이더만으로 표면, 회전, 대기 표현을 처리할 수 있음
- 기존 UV sphere는 2×1 텍스처 매핑은 쉽지만 각진 외곽선, mipmap 샘플링 손실, 반복 패턴, seam, 극점 왜곡, 대기 halo 한계가 함께 생김
- 디스크 방식은 중심 UV에서 구 표면 위치를 복원한 뒤
asin 기반 구면 UV, 회전 행렬, u좌표 이동 spin으로 구처럼 보이는 표면을 픽셀 단위로 만듦
- 셰이딩은 표면 위치를 normal로 쓰는 Lambertian 조명에서 출발하고, 암석 표면에는 TBN 행렬과 normal map을 적용하며, 대기는 halo 밝기와 채널별 흡수를 근사함
- Substance Designer 정적 텍스처와 Unreal 동적 렌더링 텍스처를 같은 방식으로 다룰 수 있어, 더 작은 동적 텍스처와 tiling 조정으로 성능 부담을 줄일 수 있음
스카이박스 행성의 시각 목표
- 프로젝트의 스카이박스는 애니메이션 달과 가스 행성으로 외계 하늘 같은 구성을 만드는 데 초점을 둠
- 두 행성은 회전하고, 가스 행성에는 움직이는 대기 흐름도 들어감
- 실제 움직임은 미묘하지만, 시각적 효과를 위해 속도를 높임
- 미술 방향은 1970~80년대 SF 아트에서 영감을 받은 반사실적(semi-realistic) 스타일임
표면 텍스처 입력과 제약
- 가스 행성 표면은 픽셀 셰이더와 render-to-texture 방식으로 실시간 생성함
- 달 텍스처는 Substance Designer에서 만들며, 색상 팔레트와 material seed를 바꿔 다양한 look을 실험할 수 있음
- 동적 가스 행성은 매 프레임 텍스처를 렌더링해야 해 성능 비용이 커짐
- 텍스처 해상도를 2배로 올리면 렌더링 비용은 4배가 됨
- 정적 Substance Designer 텍스처와 Unreal에서 동적으로 렌더링한 텍스처 모두에 일관되게 동작하는 해결책이 필요함
- cubemap은 처음에 고려됐지만 정사각형 텍스처 대비 6배 리소스를 쓸 수 있고, Substance Designer가 cubemap 생성을 지원하지 않아 실용적이지 않음
UV sphere에서 드러난 한계
- UV sphere는 2×1 직사각형 텍스처와 좌표를 맞추기 쉬워 기본 선택지처럼 보임
- 한 번에 구의 한쪽 면만 보이므로 전체 고유 직사각형 텍스처 대신 타일링 정사각형 텍스처를 쓸 수도 있음
- 실제 적용에서는 여러 문제가 겹침
- 외곽선이 눈에 띄게 각져 보이며, subdivision을 늘리거나 픽셀 셰이더 마스크로 가려야 함
- 구 표면 대부분이 비스듬히 보여 낮은 mipmap에서 샘플링되고, 동적 텍스처에서는 매 프레임 생성한 픽셀 상당수가 거의 보이지 않는 영역에 쓰임
- tiling은 정수 배수로 늘려야 전체 패턴이 이어지며, 2배만 늘려도 반복이 눈에 띔
- fractional tiling은 북극에서 남극까지 이어지는 seam을 만듦
- 극점에서는 texture pinching과 삼각형 매핑 왜곡이 생기며, PSX의 흔들리는 텍스처 같은 왜곡과 비슷하게 보임
- 표면만 그릴 수 있어 atmospheric halo에는 별도 모델이 필요함
- UV sphere는 행성 모델링에는 여전히 쓸 수 있지만, 스카이박스 용도에서는 수정과 hack이 많아짐
원형 디스크와 픽셀 셰이더 접근
- 스카이박스 행성은 멀리서 한 위치에서 관찰되므로 복잡한 3D 구 메시가 꼭 필요하지 않음
- 단순한 채워진 원형 폴리곤 디스크를 쓰고, 픽셀 셰이더에서 텍스처 매핑을 처리함
- 같은 메시 안에서 atmospheric halo까지 그릴 수 있음
- 디스크의 UV 원점은 정확히 중심에 있어야 함
- 행성 반지름을 1로 보면, 대기를 그릴 공간을 위해 UV 좌표는 그보다 바깥까지 확장돼야 함
구 표면 복원
- 좌표계는 DirectX와 일관된 left-handed, Y-up 시스템을 사용함
- Unreal Engine도 left-handed지만 up 방향은 Z이므로 방향과 normal map 포맷을 확인해야 함
- 표면 방정식은 디스크 평면의 2D 위치를 구 표면의 3D 위치로 매핑함
- 먼저 픽셀이 구 표면 안에 있는지 판단하기 위해 UV 벡터 길이가 반지름보다 작은지 검사함
float CircleMask( float2 uv, float radius)
{
return length(uv) < radius? 1.0: 0.0;
}
- 이 방식은 UV 좌표가 메시 중심에 정렬돼 있을 때만 올바르게 동작함
- 구 표면의 local position은 UV의 x, y를 그대로 쓰고 z만 복원함
float3 ReconstructSurface(float2 uv)
{
float zSquared = 1.0 - dot(uv, uv);
float z = sqrt(zSquared);
return float3(uv, z);
}
구면 UV 생성
- 정사각형 텍스처를 구 표면에 매핑하는 과정은 세 단계로 나뉨
- 원통에 감기
- y축에도 같은 처리 반복
- 결과를 원 형태로 변형
- x 텍스처 좌표는 원통을 감는 각도에 비례하며,
arcsine(x)를 계산한 뒤 [-π, π]를 [0, 1]로 다시 매핑함
- 구의 local width인 generatrix를 이용해 극점에서 좌표를 pinching함
float2 GenerateSphericalUV(float3 position)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2(0.5, 0.5);
return float2(uv);
}
축 기울기와 회전
- 더 자연스러운 행성 구도를 위해 pitch와 roll로 행성 축을 기울임
- SF 아트의 행성은 보통 기울어져 있어 구성이 역동적으로 보이고 극지방 ice cap을 보여줄 수 있음
- yaw는 회전 운동에 해당하며, seam 문제를 피하기 위해 별도 단계에서 처리함
- 메시 구에서는 일반적으로 행성을 행렬로 변환하지만, 디스크 방식에서는 각 픽셀이 표면 위치를 정의하므로 회전만 담은 단순한 3×3 행렬을 쓸 수 있음
float3x3 CreateRotationMatrix(float pitch, float roll) {
float cosPitch = cos(pitch);
float sinPitch = sin(pitch);
float cosRoll = cos(roll);
float sinRoll = sin(roll);
return (float3x3)(
cosRoll, -sinRoll * cosPitch, sinRoll * sinPitch,
sinRoll, cosRoll * cosPitch, -cosRoll * sinPitch,
0.0, sinPitch, cosPitch
);
}
- Unreal의 material editor는 행렬을 데이터 형식으로 지원하지 않지만 우회 방식이 있음
scale, seam, spin 처리
- 텍스처 scale을 바꾸면 원래 텍스처 가장자리에 맞춰 숨겨져 있던 UV seam이 드러남
- seam은 완전히 쉽게 제거할 수 없지만, 뒤쪽으로 옮기면 덜 보임
- 구를 position sign 기준으로 사분면으로 나누고, back-left와 back-right 사분면에 u방향 offset을 적용함
float2 GenerateSphericalUV(float3 position, float spin, float scale)
{
float leftRightSign = sign(position.x);
float frontBackSign = sign(position.z);
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * frontBackSign;
float2 generatrix = float2 (generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2 (spin, 0.5);
if(frontBackSign < 0.0)
{
uv = float2 (uv.x + 1.0 * leftRightSign, uv.y);
}
return float2 (uv / scale);
}
- seam은 여전히 북극과 남극을 잇지만, 뒤쪽으로 이동해 훨씬 덜 보임
- 회전은 구 자체를 돌리지 않고 u좌표 방향으로 표면 텍스처를 이동시켜 처리함
float2 GenerateSphericalUV(float3 position, float scale, float spin)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 * scale + float2(0.5 + spin, 0.5);
return float2(uv);
}
float2 sphericalVU = GenerateSphericalUV(position, scale, time*speed)
표면 셰이딩과 normal map
- 행성이 둥글게 보이려면 셰이딩이 중요함
- 일반 엔진 셰이딩과 달리 행성은 표면과 대기라는 두 레이어를 가지므로, 두 레이어의 조명과 blending을 수동으로 정의해야 함
- 반지름이 1인 경우, 회전 전 표면 위치는 곧 surface normal로 쓸 수 있음
- 조명은 단순한 Lambertian 모델을 사용함
- 목표는 완전한 사실성이 아니라 NASA 사진에서 영감을 받은 양식화된 SF 표지 미학임
float LambertianLight(float3 normal, float3 lightDirection) {
float NdotL = max(dot(normal, lightDirection), 0.0);
return NdotL;
}
- gas giant처럼 매끈한 표면은 단순 normal로 충분할 수 있음
- 암석형 행성은 산맥, 능선, 크레이터 같은 지형을 normal map으로 시뮬레이션하는 편이 적합함
- normal map을 쓰려면 Tangent, Bitangent, Normal로 구성된 TBN 행렬이 필요함
- 구에서는 이 계산을 픽셀 단위로 수행해야 함
- TBN 행렬은 normal map의 normal을 World Space Coordinates로 변환하는 데 쓰임
UV 불연속과 mipmap 아티팩트
- 픽셀 셰이더에서 생성한 UV에는 불연속(discontinuity) 문제가 생길 수 있음
- back seam은 수평 UV 성분이 경계에서 0.0과 1.0으로 완전히 이어져야 하지만, 실제로는 seam을 따라 blocky artifact가 생김
- 이 선은 UV의 DDX와 대응함
- DDX와 DDY는 화면 공간 축에서 UV 변화율을 측정함
- 텍스처 샘플러는 이 값을 이용해 사용할 mipmap을 결정함
- 낮은 UV derivative는 고해상도 mipmap에, 높은 derivative는 저해상도 mipmap에 대응함
- seam 좌우의 값이 급격히 뛰면 DDX가 커지고, 가장 낮은 mipmap에서 샘플링돼 회색 선처럼 보일 수 있음
- 행성에서는 polar patch로 가려질 예정이라 큰 문제가 아니지만, 눈에 띄는 경우에는 seam의 DDX와 DDY를 수동 보정해 sampler에 전달해야 함
- 예시 fix는 설명용이며, 실제 값은 mapping에 맞도록 수식을 직접 유도해야 함
극지 패치와 pinching 보정
- 극점 왜곡은 검증된 방식대로 polar patch로 덮음
- 행성이나 달에서는 극지방이 얼음으로 덮여 있고 다른 지역과 시각적으로 구분되는 경우가 많아, 별도 텍스처 패치가 문제를 숨기면서 시각적 흥미도 더함
- polar patch는 회전축에 수직인 평면에 매핑하고 결과 좌표를 회전시키는 방식으로 추가할 수 있음
float2 PolarPatchMapping(float3 position, float scale, float spin)
{
float cosSpin = cos(spin);
float sinSpin = sin(spin);
float scale = 0.4;
float2 uv = float2(position.x, position.z) * scale;
float2 spinningUV = float2(uv.x * cosSpin - uv.y * sinSpin,
uv.x * sinSpin + uv.y * cosSpin);
return spinningUV;
}
- UV map을 정점에 저장하지 않고 픽셀마다 생성하기 때문에, 극점 pinching 보정의 유연성이 커짐
- 간단한 나눗셈으로 극점 텍스처의 aspect ratio를 보정할 수 있음
uv = float2(uv.x, uv.y/pow(width, 1/3));
- 보정 결과는 다른 artifact처럼 보일 수 있지만, 현실적인 장면에서는 polar patch와 더 매끄럽게 이어지고 시각적 불일치가 줄어듦
대기 halo 근사
- 디스크에서 행성 표면이 전체 영역을 차지하지 않기 때문에, 남는 공간에 atmospheric halo를 그릴 수 있음
- 같은 셰이더 안에서 halo를 그리면 행성 위 대기와 자연스럽게 합칠 수 있음
- 물리 기반 대기 렌더링은 보통 Rayleigh scattering과 raymarching을 사용하지만, 여기서는 쓰지 않음
- Quake 1·2의 동적 오브젝트 조명 trick과 비슷한 근사를 사용함
- Quake의 static lighting은 lightmap으로 미리 계산됐음
- 동적 몬스터는 아래쪽 lightmap을 샘플링해 색을 조절했고, 물리적으로 정확하지는 않아도 주변과 어울리게 만들었음
- 같은 방식으로 행성 표면을 이용해 대기 halo의 조명을 근사함
- bump mapping 전 표면 normal을 확장해 halo 밝기를 계산함
- 채널별 밝기를 다르게 remap하면 서로 다른 빛 파장 흡수를 단순하게 시뮬레이션할 수 있음
- 대기는 비스듬한 각도에서 더 잘 보이므로 surface normal의 Z 성분을 remap함
- halo fade 효과를 위해 구 표면으로부터의 거리도 계산함
- 마지막에는 표면과 대기를 alpha blending으로 합침
최종 결과와 활용
- 예상보다 많은 단계가 필요했지만, 최종 결과는 완전한 구처럼 보이며 원하는 텍스처·셰이더 조작을 지원함
- Substance Designer graph 하나로 표면 텍스처를 만들 수 있고, 결과물에 추가 후처리가 필요하지 않음
- 애니메이션 텍스처도 같은 방식으로 다룰 수 있음
- 더 작은 텍스처 크기를 사용해 성능을 아낄 수 있음
- 필요한 만큼 tiling을 조정할 수 있음
- 결과 영상은 Video 3에서 확인할 수 있음