The water effects in Super Mario Sunshine: How it works
[!info] 본 글은 ‘How it works’와 ‘implementation’ 두 편으로 구성된 시리즈 중 첫 번째 편이다.
이 ‘How it works’ 편에서는 게임 ‘Super Mario Sunshine(2002)‘의 물 그래픽 구현을 기술적 관점에서 해부한다.
2002년에 발매된 게임 ‘Super Mario Sunshine(정발명: 슈퍼 마리오 선샤인)‘는 특히 물(water) 그래픽이 훌륭하기로 정평이 나있는 작품이다.
최근 이 작품을 다시 플레이하게 됐는데 문득 구현 원리가 궁금해 조사해보았고, 직접 간단히 구현도 하게 되었다.
History of implement water effects
In early CG
계산 수식과 각종 이미지 소스들을 이용해 인위적 상(screen)을 창조하는 컴퓨터 그래픽스 분야에서 물은 구현하기 상당히 까다로운 존재였다.
기술이 없는 건 아니었지만, 원활한 FPS(초당 프레임 수)를 유지해야하는데 물의 특성은 당시 보급된 하드웨어의 성능에 비해 너무도 많은 계산과 리소스를 요구했다.
결국 초기의 실시간 3D 게임들은 물의 ‘까다로운’ 시각적 특성을 구현하는 건 포기하고, 단지 표면에 텍스처를 입히고 내부(수면 아래)는 반투명하게 처리한 후 이렇게 주장했다: “이건 물입니다.”

Super Mario 64 (1996)

Wave Race 64 (1996)
In modern CG
하드웨어 성능이 비약적으로 성장함에 따라, 현대의 실시간 렌더링 환경에서는 물의 복잡한 시각적 특성—파동, 반사, 굴절, 투명도—을 고해상도와 높은 Frame rate로 사실적으로 구현할 수 있게 되었다.
오늘날의 게임 엔진들은 실시간 환경맵, PBR(Physically Based Rendering), shader 기반의 다양한 효과를 통해 물 표현의 사실성을 극대화하고 있다.
하지만 이러한 발전의 시작점에는, 제한된 하드웨어에서 창의적인 방법으로 사실적인 물을 구현하고자 했던 선구적인 시도들이 존재한다.
본 글에서는 그 터닝포인트라 할 수 있는 2002년작 ‘Super Mario Sunshine’의 물 효과에 초점을 맞춰, 당시의 한계 속에서 어떤 기술적 돌파구가 마련되었는지 살펴본다.

Unreal Engine 5 (2022)

The Legend of Zelda: Breath of the Wild (2017)
Turning point: Super Mario Sunshine
‘Super Mario Sunshine’은 위의 ‘In early CG’에서 소개된 게임들의 발매에서 고작 6년 밖에 안 지나 출시됐던 게임이다.
그런데 물 표현에 있어 이만큼 비약적으로 성장한 것이다.
도대체 어떻게 한 걸까?
현대의 물 표현과는 어떤 점이 다른 걸까?
Nintendo GameCube (2001)

Super Mario Sunshine은 닌텐도의 가정용 콘솔 게임기 ‘GameCube(정발명: 게임큐브)‘의 타이틀이다.
GameCube의 Graphics Processor는 ATI사의 Flipper라는 칩으로, 오직 GameCube만을 위해 개발된 칩이다.
그리고 이 칩의 중요한 특징은 fixed-function pipeline만을 지원하도록 설계되어있다는 것이다. 즉 Non-programmable하며, 모든 연산은 하드웨어에 이미 박혀있는 회로대로만 동작한다. (사실, 90년대 후반에서 00년대 초반까지는 fixed-function pipeline이 표준이었다. 콘솔 게임기, PC 모두!)
Programmable & Non-programmable
현대 그래픽스 API에서, 프로그래머들은 자신이 원하는 효과를 구현하기 위해 graphics pipeline 중 실행할 custom program인 shader를 직접 작성할 수 있다. 즉 graphics pipeline의 일부를 직접 프로그래밍할 수 있다. 이 점에서 ‘programmable하다’고 하는 것이다. Graphics pipeline with Vertex & Fragment shader
이처럼 vertex 처리와 fragment 처리가 programmable하다면 물의 사실적 표현을 위해 무엇을 할 수 있을까?
Characteristics of Water
여기서 이것을 짚고 넘어가자: 물(water)처럼 보이려면 뭘 구현해야 하는가?
구현하고자 하는 대상이 있다면 우선 그 명세를 철저히 해야한다. 그러므로 우리는 ‘물’이 특히 시각적 측면에서 어떤 특징을 갖고 있는지 정리할 것이다.
Wikipedia 문서 water의 대표 이미지는 물의 시각적 특징을 잘 보여주고 있다.

- wave(파동): 물은 외부의 힘에 의해 다양한 물결, 즉 파동이 생긴다.
- reflection(반사): 물 표면은 주변 사물과 빛을 반사한다.
- refraction(굴절): 물은 빛을 굴절시켜 수면 아래의 것들을 왜곡되어 보이게 한다.
- transparency (투명도): 물은 투명하여 내부가 비쳐보인다.
이 네 가지 특징이 구현되어야 비로소 ‘사실적인 물’로 보인다는 점을 숙지하고 다음을 읽어나가자.
Implement water effects in programmable pipeline

- vertex shader
- 각 vertex의 시간/공간적 값에 따라 좌표를 움직이도록 코드를 작성하면 wave(파동)을 연출할 수 있다.
사실적인 물을 표현하기 위해선 특별한 shading이 필요하므로 fragment shader가 할 일은 더 많다. reflection(반사)와 refraction(굴절)를 구현하기 위해선 반사각과 굴절각을 고려해야한다.
- Fragment shader
- 뷰 벡터와 표면의 노말 벡터를 이용해 반사 벡터를 구할 수 있다. 그리고 해당 방향으로 주변 오브젝트 및 환경맵을 샘플링하면 reflection(반사)를 표현할 수 있다.
- 이미 알려진 공기-물 굴절률을 이용해 굴절 벡터를 구할 수 있다. 그리고 해당 방향으로 수면 아래를 왜곡하여 샘플링하면 refraction(굴절)을 표현할 수 있다.
- 각종 수치값에 따라 동적으로 alpha값을 조절함으로써 transparency(투명도)를 표현할 수 있다.
이는 별도의 꼼수(trick)를 사용하지 않는, Physically Based Rendering(PBR)에서 제시하는 ‘표준적인’ 방법에 가깝다.
Implement water effects in non-programmable pipeline
만약 graphics pipeline이 non-programmable하다면, vertex/pixel별로 이러한 custom 연산을 할 수 없다. 오직 물 표현을 위한 하드웨어가 마련되어있지 않은 이상 말이다.
그러나 하드웨어가 non-programmable함에도 불구하고. Super Mario Sunshine는 이런 custom 연산 없이 ‘그럴 듯하게 사실적인’ 물을 구현했다.
How they implement
GameCube가 지원하는 주요 그래픽 기능은 다음과 같다:
- Basic color and lighting
- UV animation and distortion: Fixed-function texture matrix/UV transformations allowed
- Screen texture (Render-to-Texture): The entire screen image could be copied to a texture
- Multi-texturing: Up to 8 textures could be combined in a single rendering pass
- Alpha blending
- Alpha-test
이를 이용해 물의 네 가지 특징을 어떻게 구현했는지 살펴보자.
Wave
Texture scrolling
Texture scrolling is a technique where you add an offset to the UV coordinates of vertices or pixels over time, making it look like the texture is flowing or moving across the surface. Texture scrolling is essentially a texturing trick that utilizes UV mapping to create the illusion of motion on a static object.
아래는 게임 속 스테이지 중 하나인 Delfino plaza의 물 표현 구현에서 실제 사용된 세팅 파일의 일부이다. 같은 텍스처 파일을 사용하되 XY offset(움직임의 방향)을 달리한 것을 확인할 수 있다.
Textures:
- B_ENKEIwave_s
Offset: 0.7182068824768066, 0.7182068824768066
Center: 0, 0
Scale: 1, 1
Rotation: 0
- B_ENKEIwave_s
Offset: -0.31095582246780396, 0.31095582246780396
Center: 0, 0
Scale: 1, 1
Rotation: 0
Reflection
실제 물리 현상과는 별개로, 컴퓨터 그래픽으로 구현한다는 측면에서 생각하면 해야하는 것은 두 가지이다:
- 빛의 반사 (Specular Highlight)
- 주변 환경의 반사 (Environment Reflection)
빛의 반사: 수면이 햇빛을 반사하듯이 밝게 빛난다
주변 물체의 반사: 수면이 주변 환경을 반사하고 있다
(수면 위의 동전과 소금쟁이 적은 실시간으로 움직이는 동적인 요소이다.)
[!Note] 현대의 게임 엔진(PBR, UE, Unity)에서도 ‘빛의 반사’와 ‘주변 물체의 반사’ 두 효과는 분리되어 구현된다.
(1) 빛의 반사 (Specular Highlight)
우선 빛의 반사를 생각해보자.
바다의 수면을 생각해보면, 햇빛이 표면에 반사될 때 빛이 넓게 퍼지기 보다는 아주 밝고 가느다란 띠(band)를 형성하곤 한다.
벡터 연산 없이 이를 표현하기 위해 Super Mario Sunshine에서는 ‘그럴듯한’ 꼼수를 사용했는데 바로 Mipmap과 Alpha-test이다.
이러한 mip-mapping으로 인해 아주 가까이에서는 투명하게 보이고, 중간 거리에서는 밝게 보이다가, 아주 멀어지면 다시 투명하게 된다.

mip-mapping이 적용된 결과
LOD는 floating-point 값으로, 이 값을 토대로 interpolate(보간)하여 텍스처가 선형적으로 매핑되기 때문에 경계가 흐릿하고 지저분한 감이 있다.
여기서 가장 밝게 빛나는 부분, 하이라이트 띠(band)를 극적으로 강조하기 위해선 경계를 명확히 해야한다. 따라서 Alpha-test를 수행하여 밝기가 특정 임계값을 넘기는 픽셀만을 그리고 나머지는 버린다. 버리는 픽셀의 위치에는 아무것도 그리지 않기 때문에 투명하게 보인다.

mip-mapping이 적용된 결과에 Alpha-test을 수행한 결과
(2) 주변 환경의 반사 (Environment Reflection)
그 다음으로 주변 물체의 반사를 살펴보자. Render stage diagram 우측은 EFB에 출력된 이미지, 좌측은 EFB 이미지를 텍스처로 활용하여 최종 렌더된 이미지이다. 이를 이용하여 위와 같이 주변 환경이 반사된 이미지를 렌더할 수 있다.
다행스럽게도, GameCube는 하드웨어적으로 이를 지원한다.
이처럼 Flipper의 EFB를 이용한 Render-to-Texture 방식은 제한된 자원 내에서 비교적 사실적인 반사를 가능하게 해 주었다.
[!Note] 이러한 Render-to-Texture Reflection는 Super Mario Sunshine 이전의 3D 게임들에서도 쓰였던 보편적인 기법이며, 이 게임만의 독창적인 기법은 아니다.
Refraction
refraction(굴절) 또한 EFB를 사용한다.
EFB에 수면 아래의 지형을 포함하여 렌더링하고, 그 이미지를 텍스처로 사용한다. 이때 텍스처의 UV좌표를 물결 무늬에 맞춰 흔들면 수면 아래 풍경이 일그러져 보이는 효과를 줄 수 있다.
앞서 소개했던 wave 텍스처의 밝기 값을 UV좌표의 offset으로 삼아 EFB 복사본을 샘플링하면 그럴 듯한 일그러짐이 연출된다.
GLSL 코드로 작성하면 다음과 같다.
// 흑백(1-channel) wave 텍스처 사용
vec2 waveUV = uv + u_time * waveSpeed;
float waveVal = texture(waveTex, waveUV).r;
float refractOffsetValue = (waveVal - 0.5) * strength; // [0, 1] 범위의 값을 [-0.5, 0.5]로 매핑 후 임의 강도를 곱한다
vec2 refractedOffset = vec2(refractOffsetValue, refractOffsetValue); // vec2로 만듦
refractedColor = texture(EFB_Copy, uv + refractOffset);
Transparency
transparency(투명도)는 사실 앞에서 이미 다뤘다.
이로써 플레이어는 주변의 수면 아래를 투명하게 들여다볼 수 있다.
Vertex Painting and Color Effects (Extra)
이것은 mesh의 버텍스에 색을 지정하는 vertex painting을 사용해 표현한 것이다.
경계가 되는 부분을 정의하고 해당 부분의 버텍스에 색상을 부여하면 그 사이의 픽셀은 선형적으로 interpolate된다.
vertex painting은 1차적으로 EFB에 렌더링될 때 적용된다.

1차적으로 EFB에 렌더된 이미지: vertex painting이 적용됨
References
- YouTube: Recreating the Water Effect from Super Mario Sunshine in Unity (exp1Yrxt50A)
- PC Perspective: The Graphics Technology of Super Mario Sunshine
- mechEYE blog: Deconstructing the Water Effect in Super Mario Sunshine
- Dolphin Emulator Blog: Add Heuristic for Detecting Custom Mipmaps
- The Models Resource: Super Mario Sunshine – Water (Object)
- Reddit: I’m trying to recreate the water effect from Super Mario Sunshine (Blender Help)
- YouTube: Super Mario Sunshine Water Effect Breakdown (8rCRsOLiO7k)
- YouTube: Super Mario Sunshine’s Water is Wild (Ipsd6rYj6Mk)
- Copetti: GameCube Architecture and Analysis