1P by GN⁺ 5시간전 | ★ favorite | 댓글 1개
  • 게임보이 컬러에서 실시간 3D 셰이딩을 구현한 프로젝트로, 플레이어가 빛의 궤도를 조작하며 물체를 회전시킬 수 있음
  • 정규화 벡터와 램버트 셰이딩(dot product) 계산을 기반으로, 구면좌표계를 이용해 연산을 단순화함
  • 곱셈 명령이 없는 SM83 CPU의 제약을 극복하기 위해 로그 변환과 룩업 테이블을 활용, 8비트 정밀도로 연산 수행
  • 자기 수정 코드(self-modifying code) 를 사용해 약 10%의 성능 향상을 달성하고, 프레임당 15개의 타일을 렌더링함
  • AI를 활용한 코드 생성은 대부분 실패했으며, 핵심 알고리듬과 셰이더는 직접 작성한 수작업 코드로 완성됨

프로젝트 개요

  • 게임보이 컬러에서 실시간으로 이미지를 렌더링하는 게임을 제작
    • 플레이어는 궤도 형태의 빛을 조작하며 물체를 회전시킴
  • 전체 코드는 GitHub 저장소(nukep/gbshader)에서 공개됨

3D 제작 과정

  • Blender를 사용해 초기 룩 개발(lookdev)을 진행, 결과가 시각적으로 만족스러워 프로젝트를 진행함
  • Cryptomatte와 커스텀 셰이더를 이용해 노멀맵(normal map) 을 생성
    • 찻주전자(teapot) 모델은 카메라를 회전시켜 PNG 시퀀스로 노멀맵을 출력
    • 게임보이 컬러 모델의 화면 부분은 별도 장면으로 렌더링 후 합성

수학적 기반

  • 노멀맵은 각 픽셀의 법선 벡터를 인코딩하는 벡터 필드로 사용
  • 램버트 셰이딩v = N·L 형태의 내적(dot product)으로 계산
  • 구면좌표계로 변환해 v = sinNθ sinLθ cos(Nφ−Lφ) + cosNθ cosLθ 형태로 단순화
    • 모든 벡터의 반지름 r=1로 가정해 연산량을 줄임

게임보이에서의 구현

  • Lθ(빛의 세로각) 을 상수로 고정하고, Lφ(빛의 회전각) 만 플레이어가 조작
  • ROM에는 각 픽셀을 (Nφ, log(m), b) 형태로 저장
  • 곱셈 명령 부재를 해결하기 위해 로그 변환과 룩업 테이블(log, pow)을 사용
    • 부호(sign) 비트를 상위 비트에 저장해 음수 연산 지원
  • 모든 스칼라 값은 -1.0~+1.0 범위의 8비트 분수로 표현
    • 덧셈은 선형 공간에서, 곱셈은 로그 공간에서 수행
    • 127을 분모로 사용해 ±1을 모두 표현 가능하게 함

cos_log와 핵심 연산

  • cos_loglog(cos x) 형태의 결합 룩업으로, 곱셈을 로그 덧셈으로 대체
  • 픽셀당 연산량
    • 뺄셈 1회, cos_log 조회 1회, 덧셈 1회, pow 조회 1회, 덧셈 1회
    • 총 3회 덧셈/뺄셈, 2회 룩업 수행

성능

  • 프레임당 15개의 타일을 처리, 일부 빈 행은 더 빠르게 계산
  • 픽셀당 약 130 사이클, 빈 행은 3 사이클 소요
  • CPU의 약 89% 가 셰이더 연산에 사용되며, 나머지는 입력 및 I/O 처리에 사용

자기 수정 코드(Self-Modifying Code)

  • 프레임당 약 960픽셀을 처리하는 핵심 루프를 최적화하기 위해 명령어 자체를 수정
    • 상수를 직접 코드에 삽입해 변수 로드보다 빠른 연산 수행
    • 예: sub a, 8sub a, variable보다 12사이클 빠름
    • 전체적으로 약 11,520 사이클(10%) 절감

AI 활용 시도

  • 전체 프로젝트의 95%는 수작업으로 작성
  • AI는 Game Boy 어셈블리(SM83) 작성에 어려움을 겪음
  • AI 사용 내역
    • Python: OpenEXR 레이어 읽기
    • Blender: 장면 자동화 스크립트
    • SM83: 일부 기능 스니펫 (예: VRAM DMA)
  • 실패한 시도
    • AI로 셰이더 어셈블리 코드 생성 시도 → 비효율적이고 오류 다수
  • Claude Sonnet 4 모델을 이용해 의사코드에서 어셈블리 생성 시도
    • 일부 작동했으나 느리고, Z80과 SM83 혼동 등의 오류 발생
    • 최종 코드는 수동으로 완전 재작성

결론 및 교훈

  • AI는 단순 스크립트에는 유용하지만, 정확성과 검증이 필수
  • OpenEXR 처리 코드에서 AI가 채널 정렬 오류(BGR vs RGB) 를 일으켜 수주간 버그 발생
  • 경험을 통해 “AI 사용 시 검증이 가장 중요하다”는 교훈을 강조
  • 프로젝트는 레거시 하드웨어의 한계를 극복한 실험적 셰이더 구현 사례로 평가됨
Hacker News 의견들
  • 진짜 해커 감성의 글을 HN에서 보게 되어 반가운 마음임

    • 단순히 AI 프롬프트로 만든 게 아닌가 궁금했음. 어떻게 구현했는지 알고 싶음 😉
  • 결과물이 정말 멋짐. 내가 이해하기로는 이건 “3D처럼 보이지만 실제로는 2D 노멀맵을 미리 렌더링해 조명 효과를 입힌 셰이더”임
    프레임들은 여기 GitHub 링크에 있음

    • 사실상 “진짜 3D” 렌더러와 크게 다르지 않음. deferred 렌더링 파이프라인에서도 깊이맵, 노멀맵, 컬러 버퍼 등 2D 버퍼 위에서 셰이더가 작동함.
      3D 삼각형 처리 부분은 단순하게 유지하고, 비싼 조명 셰이더는 2D 이미지 위에서 한 번만 실행되므로 효율적임
      셰이더 입장에서 입력이 3D 벡터라면 그건 3D 셰이더임. 3D 래스터라이저가 있느냐는 별개의 문제임
      현대 3D 게임들도 이런 방식을 다양하게 활용함. 여러 시점에서 미리 렌더링한 모델을 사용하는 imposter 기법도 정식 3D 엔진에서 쓰이는 기술임
    • 예전 Mac 게임들이 3D 가속 하드웨어 없이 2D 텍스처에 조명을 입히던 방식과 비슷함.
      다만 이번엔 그게 Gameboy Color에서 돌아간다는 점이 놀라움
  • 안녕하세요, 작성자임. 여기에 글이 올라왔다고 들어서 계정을 만들었음. 공유해줘서 고마움
    환경 맵을 이용해 더 단순화하는 실험도 진행 중이며, Bsky에 공유한 링크에서 볼 수 있음

  • 정말 흥미로운 프로젝트임. 예전에 C64 어셈블리 코딩하던 시절이 떠오름.
    당시에도 곱셈 명령이 없어서 하드웨어 제약을 우회하는 창의적인 방법을 찾아야 했음

  • AI를 사용해보려는 시도였지만 결과적으로는 실패한 실험이었음.
    업계가 AI 얘기로 떠들썩해서 직접 체험해보고 싶었고, 생성형 AI 사용 여부를 투명하게 공개하는 게 중요하다고 생각함.
    숨기면 신뢰를 해치고, 공개하면 다른 의견을 가진 사람들과도 열린 대화를 할 수 있음

    • 원래는 중립적인 어조였는데, 사람들이 내가 AI를 찬양한다고 오해해서 약간 회의적으로 바꿨음.
      단지 이 과정을 기록하고 싶었을 뿐임
  • 이 GBC 셰이더는 “모든 계산은 제약 속의 근사치”라는 진리를 보여줌.
    곱셈은 테이블 조회와 덧셈으로 대체되고, 정밀도는 눈으로 보이는 결과에 맞춰 조정됨

  • 진짜 감탄스러움. 특히 이게 실제 Game Boy Color 하드웨어에서 돌아간다는 점이 놀라움.
    종종 카트리지에 강력한 프로세서를 넣고 GBC를 단순 터미널로 쓰는 경우가 많은데, 이건 그런 해킹이 아님

  • 솔직히 Nintendo가 GBC나 GBA를 재출시해줬으면 좋겠음.
    몇 개의 게임을 내장한 카트리지 형태로 팔면 바로 살 의향이 있음

    • 중고로는 꽤 저렴하게 구할 수 있음. 플래시 카트리지를 추가하면 끝임.
      다만 요즘은 같은 폼팩터의 안드로이드 휴대용 기기가 더 실용적임.
      나도 Gameboy 컬렉션이 있지만, 요즘은 에뮬레이터가 훨씬 편함
    • Oculus VR 창업자가 만든 ModRetro Chromatic을 사면 됨.
      Nintendo가 새로 만든다 해도 이만큼 좋을 순 없을 것 같음
  • 이런 글이야말로 HN이 존재하는 이유임.
    예전 기술 잡지를 넘기던 때의 즐거움을 다시 느끼게 함

  • 이 작성자는 좋은 의미에서 미친 천재