Nintendo 64의 가산 혼합(Additive Blending) 문제 해결하기
많은 레트로 게임 팬들에게, 5세대 콘솔에서 시각적 효과—특히 폭발, 플라즈마 빔, 마법 주문—가 처리되는 방식에는 항상 눈에 띄는 차이가 있었습니다. PlayStation (PSX)의 Silent Bomber와 같은 타이틀을 Nintendo 64 (N64)의 Star Fox 64와 비교해 보면, PSX의 효과가 종종 더 "luminous"하고 생동감 있게 느껴집니다.
이러한 시각적 차이는 예술적 선택의 문제가 아니라, 하드웨어가 가산 혼합을 처리하는 방식의 근본적인 차이 때문입니다. N64는 기술적으로 이 기능을 지원했지만, 결정적인 하드웨어적 누락으로 인해 당시 개발자들에게는 사실상 사용이 불가능했습니다.
가산 혼합의 메커니즘
가산 혼합은 소스 픽셀(그려지는 스프라이트나 효과)의 색상 값이 목적지 픽셀(기존 프레임 버퍼)의 색상 값에 더해지는 과정입니다. 수학적으로 이는 result = src + dst로 표현됩니다.
결과값이 더해지는 방식이기 때문에, 출력은 항상 더 밝아질 뿐 결코 어두워지지 않습니다. 이는 빛 기반 효과를 시뮬레이션하는 데 이상적인 기술입니다. PlayStation에서는 GPU가 이를 매끄럽게 처리했습니다. 빨강, 초록, 또는 파랑 채널의 합이 가능한 최대값(예: 8비트 시스템의 255)을 초과하면, PSX GPU는 단순히 그 값을 255로 "clamp"합니다.
N64의 치명적 결함: 정수 랩어라운드(Integer Wrap-around)
Nintendo 64는 매우 유연한 "Color Combiner"를 특징으로 하는 Reality Display Processor (RDP)를 사용했습니다. 이를 통해 개발자들은 (P * A) + (Q * B)와 같은 공식을 사용하여 복잡한 혼합 연산을 정의할 수 있었습니다.
N64에서 가산 혼합을 설정하는 것은 코드상으로는 매우 간단합니다. libdragon 라이브러리를 사용하면 다음과 같습니다:
RDPQ_BLENDER(( IN_RGB, IN_ALPHA, MEMORY_RGB, ONE ))
하지만 RDP에는 결정적인 결함이 있습니다: 결과값을 clamp하지 않습니다. 최대값에서 멈추는 대신, N64의 하드웨어는 값이 랩어라운드(wrap around)되도록 허용합니다. 색상 값이 256에 도달하면 0으로 리셋됩니다.
실제로 이는 폭발의 가장 밝은 부분—순수 흰색이어야 할 부분—이 갑자기 검은색이나 어두운 색으로 변해버려, 효과를 망치는 눈에 거슬리는 시각적 아티팩트(artifacts)를 생성한다는 것을 의미합니다.
해결책 설계하기
랩어라운드 없이 진정한 가산 혼합을 구현하기 위해서는, 개발자는 소스와 목적지 픽셀의 합이 버퍼의 최대값을 초과하지 않도록 보장해야 합니다. 여기에는 두 가지 주요 접근 방식이 있으며, 각각 고유한 트레이드오프가 있습니다.
RSP 접근 방식
N64의 벡터 코프로세서인 Reality Signal Processor (RSP)를 사용하여 혼합과 클램핑을 수동으로 처리할 수 있습니다. 하지만 RSP는 래스터화(rasterization)를 위해 설계된 것이 아닙니다. 회전, 스케일링, 또는 3D 지오메트리를 위해 RSP를 사용하는 것은 RDP를 사용하는 것보다 훨씬 더 복잡합니다.
32비트 버퍼 전략
더 우아한 해결책은 프레임 버퍼의 정밀도를 조작하는 것입니다. 대부분의 N64 게임은 메모리 대역폭을 아끼기 위해 최종 출력용으로 16비트 프레임 버퍼를 사용했습니다. 대신 32비트 RGBA 8888 버퍼에 렌더링함으로써, 개발자는 훨씬 더 많은 여유 공간(headroom)을 확보할 수 있습니다.
32비트 버퍼에서도 값이 랩어라운드되는 것을 방지하기 위해, "intensity scaling"이라는 기술을 사용할 수 있습니다. 컬러 컴바이너에 모든 색상을 1/8 강도로 그리기(fog alpha 값을 곱셈 인자로 사용)라고 지시함으로써, 결과적인 가산 합계는 랩어라운드 임계값보다 훨씬 낮은 수준으로 유지됩니다.
// Abuse the fog alpha value to draw all colors at 1/8th intensity
rdpq_set_fog_color(RGBA32(0, 0, 0, 256/8));
rdpq_mode_blender(RDPQ_BLENDER(( IN_RGB, FOG_ALPHA, MEMORY_RGB, ONE )));
생명력을 불어넣기: 최종 변환
32-bit 버퍼는 랩어라운드를 방지하지만, 이미지는 이제 너무 어두워졌습니다. 마지막 단계는 32비트 버퍼를 디스플레이용 16비트 RGBA 5551 버퍼로 다시 변환하는 것입니다. 이 과정에서 5비트 범위(0-31)를 초과하는 모든 값을 클램핑(clamping)합니다.
이 변환을 CPU에서 수행하는 것은 매우 느립니다(프레임당 약 70ms). 하지만 RSP의 128-bit 벡터 인스트럭션을 활용하면, 변환을 약 프레임당 3.1ms로 최적화하여 실행할 수 있습니다. 오늘날 홈브루(homebrew) 개발자들에게는 RSPL(RSP 마이크로코드를 컴파일하는 C-like 언어)과 같은 현대적인 도구들을 통해 이 구현이 가능해집니다.
트레이드오프와 하드웨어적 제약
\n이 기술은 "공짜 점심"이 아닙니다. N64 개발자들이 { "type": "object", "properties": { "body": "# Nintendo 64의 가산 혼합(Additive Blending) 문제 해결하기\n\n많은 레트로 게임 팬들에게, 5세대 콘솔에서 시각적 효과—특히 폭발, 플라즈마 빔, 마법 주문—가 처리되는 방식에는 항상 눈에 띄는 차이는가 있습니다. PlayStation (PSX)의 Silent Bomber와 같은 타이틀을 Nintendo 64 (N64)의 Star Fox 64와 비교해 보면, PSX의 효과가 종종 더 "luminous"하고 생동감 있게 느껴집니다.\n\n이러한 시각적 차이는 예술적 선택의 문제가 아니라, 하드웨어가 가산 혼합을을 처리하는 방식의 근본적인 차이 때문입니다입니다. N64는 기술적으로 이 기능을 지원했지만, 결정적인 하드웨어적 누락으로 인해 당시 개발자들에게는 사실상 사용이 불가능했습니다.\n\n## 가산 혼합의 메커니즘\n\n가산 혼합은 소스 픽셀(그려지는 스프라이트나 효과)의 색상 값이 목적지 픽셀(기존 프레임 버퍼)의 색상 값에 더해지는 과정입니다. 수학적으로 이는 result = src + dst로 표현됩니다.\n\n결과값이 더해지는 방식이기 때문에, 출력은 항상 더 밝아질 뿐 결코 어두워지지 않습니다. 이는 빛 기반 효과를 시뮬레이션하는 데 이상적인 기술입니다. PlayStation에서는 GPU가 이를 매끄럽게 처리했습니다. 빨강, 초록, 또는 파랑 채널의 합이 가능한 최대값(예: 8비트 시스템의 255)을 초과하면, PSX GPU는 단순히 그 값을 255로 "clamp"합니다.\n\n## N64의 치명적 결함: 정수 랩어라운드(Integer Wrap-around)\n\nNintendo 64는 매우 유연한 "Color Combiner"를 특징으로 하는 Reality Display Processor (RDP)를를 사용했습니다. 이를 통해 개발자들은 (P * A) + (Q * B)와 같은 공식을 사용하여 복잡한 혼합 연산을 정의할 수 있었습니다.\n\nN64에서 가산 혼합을 설정하는 것은 코드상으로는 매우 간단합니다. libdragon 라이브러리를 사용하면 다음과 같습니다:
\nc\nRDPQ_BLENDER(( IN_RGB, IN_ALPHA, MEMORY_RGB, ONE )) \n\n하지만 RDP에는 결정적인 결함이 있습니다: 결과값을 clamp하지 않습니다. 최대값에서 멈추는 대신, N64의 하드웨어는 값이 랩어라운드(wrap around)되도록 허용합니다. 색상 값이 256에 도달하면 0으로 리셋됩니다.\n\n실제로 이는 폭발의 가장 밝은 부분—순수 흰색이어야 할 부분—이 갑자기 검은색이나 어두운 색으로 변해버려, 효과를 망치는 눈에 거슬리는 시각적 아티팩트(artifacts)를 생성한다는 것을 의미합니다.\n\n## 해결책 설계하기\n\n랩어라운드 없이 진정한 가산 혼합을 구현하기 위해서는, 개발자는 소스와 목적지 픽셀의 합이 버퍼의 최대값을 초과하지 않도록 보장해야 합니다. 여기에는 두 가지 주요 접근 방식이 있으며, 각각 고유한 트레이드오프가 있습니다.\n\n### RSP 접근 방식\n\nN64의 벡터 코프로세서인 Reality Signal Processor (RSP)를 사용하여 혼합과 클램핑을 수동으로 처리할 수 있습니다. 하지만 RSP는 래스터화(rasterization)를 위해 설계된 것이 아닙니다. 회전, 스케일링, 또는 3D 지오메트리를 위해 RSP를 사용하는 것은 RDP를 사용하는 것보다 훨씬 더 복잡합니다.\n\n### 32비트 버퍼 전략\n\n대부분의 N64 게임은 메모리 대역폭을 아내기 위해 최종 출력용으로 16비트 프레임 버퍼를 사용했습니다. 대신 3D-bit 버퍼에 렌더링함으로써, 개발자는 훨씬 더 많은 여유 공간(headroom)을 확보할 수 있습니다.\n\n32-bit 버퍼에서도 값이 랩어라운드되는 것을 방지하기 위해, "intensity scaling"이라는 기술을 사용할 수 있습니다. 32-bit 버퍼에서도 값이 랩어라운드되는 것을 방지하기 위해, "intensity scaling"이라는 기술을 사용할 수 있습니다. 컬러 컴바이너에 모든 색상을 1/8 강도로 그리기(fog alpha 값을 곱셈 인자로 사용)라고 지시함으로써, 결과적인 가산 합계는 랩어라운드 임계값보다 훨씬 낮은 수준으로 유지됩니다.\n\nc\n// Abuse the fog alpha value to draw all colors at 1/8th intensity rdpq_set_fog_color(RGBA32(0, 0, 0, 256/8)); rdpq_mode_blender(RDPQ_BLENDER(( IN_RGB, FOG_ALPHA, MEMORY_RGB, ONE ))); \n\n## 생명력을 불어넣기: 최종 변환\n\n32-bit 버퍼는 랩어라운드를 방지하지만, 이미지는 이제 너무 어두워졌습니다. 마지막 단계는 32비트 버퍼를 디스플레이용 16비트 RGBA 5551 버퍼로 다시 변환하는 것입니다. 이 과정에서 5비트 범위(0-31)를 초과하는 모든 값을 클램핑(clamping)합니다.\n\n이 변환을 CPU에서 수행하는 것은 매우 느립니다(프레임당 약 70ms). 하지만 RSP의 128-bit 벡터 인스트럭션을 활용하면, 변환을 약 프레임당 3.1ms로 최적화하여 실행할 수 있습니다. 오늘날 홈브루(homebrew) 개발자들에게는 RSPL(RSP 마이크로코드를 컴파일하는 C-like 언어)과 같은 현대적인 도구들을 통해 이 구현이 가능해집니다.\n\n## 트레이드오프와 하드웨어적 제약\n\n이 기술은 "공짜 점심"이 아닙니다. N64 개발자들이 32-bit 버퍼를 피한 주된 이유는 콘솔의 악명 높은 낮은 메모리 처리량 때문입니다. 32-bit 버퍼에 렌더링하는 것은 16-bit 버퍼와 비교했을 때 RDRAM으로 데이터를 주고받는 바이트 수를 두 배로 늘려야 하므로, 전체적인 성능에 상당한 영향을 미칠 수 있습니다.\n\n그럼에도 불구하고, 특정 애플리케이션이나 국소적인 효과를 위해 이 방법은 N64의 시각적 팔레트에서 빠져있던 "영광스러운" 가산 혼합을 제공합니다. 한 커뮤니티 구성원이 언급했듯이, 이는 포화 연산(saturation arithmetic)의 필요성을 보여주는 전형적인 사례입니다. 이는 오디오 믹싱에서도 흔히 볼 수 있는 개념으로, 범위를 벗어난 값을 클램핑하지 못하면 디지털 클리핑(digital clipping)과 거친 아티팩트가 발생합니다.\n\n고정밀도 렌더링 타겟, 강도 스케일링, 그리고 RSP 가속 클램핑을 결합함으로써, 우리는 마침내 N64에 그에 걸맞은 빛나는 폭발 효과를 줄 수 있습니다.\n