메뉴 전체보기

회원메뉴

프로젝트 게슈탈트 > 게시판

본문 바로가기

쇼핑몰 검색

NO. 3565
제목 : 프로젝트 게슈탈트
2026-10-13 10:34

소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


소개: 프로젝트 게슈탈트


여러분, 안녕하세요. 다시 찾아주셔서 감사합니다!

이것은 제가 처음부터 직접 제작한 DIY 벽걸이형 RGB 매트릭스 디스플레이인 ‘프로젝트 게슈탈트’입니다. 두 개의 64×32 RGB LED 매트릭스를 연결하여 하나의 64×64 디스플레이를 구성했으며, 모든 것은 라즈베리 파이 피코(Raspberry Pi Pico)로 구동됩니다.

이 장치는 게임 관련 애니메이션, 무작위 패턴, 그 외 여러 가지 효과를 포함하여 제가 추가한 다양한 애니메이션과 시각 효과를 표시할 수 있습니다.

전체 케이스는 Fusion 360으로 설계되었으며, 디자인은 『NieR: Automata』의 ‘Pods’에서 큰 영감을 받았습니다. ‘게슈탈트(Gestalt)’라는 이름도 여기서 유래했습니다. ‘게슈탈트(Gestalt)’는 독일어로 대략 ‘형태’나 ‘모양’을 의미하며, 개별 요소들이 모여 하나의 완전한 전체를 형성한다는 개념도 포함합니다. 두 개의 별도 RGB 매트릭스가 결합되어 하나의 완전한 디스플레이를 만드는 이 프로젝트에 딱 맞는 이름이라고 생각했습니다.

프로젝트 게슈탈트는 3.7V, 2600mAh 리튬이온 전지로 구동되며, 약 4시간의 배터리 사용 시간을 제공합니다. 총 4,096개의 RGB LED를 구동하는 장치로서는 꽤 괜찮은 수준입니다.

저는 이전에 ‘웨이브폼(WaveForm)’이라는 유사한 프로젝트를 진행한 적이 있는데, 당시에는 64×32 RGB 매트릭스 하나와 마이크를 사용하여 주변 소리를 감지하고 소리에 반응하는 파형을 디스플레이에 표시했습니다. 소리의 크기가 커지면 매트릭스에 표시된 파형도 그에 따라 반응했습니다.

이 기능이 너무 마음에 들어 ‘Project Gestalt’에도 WaveForm 코드를 구현하기로 했습니다.

기기를 제어하기 위해 ‘이전(Previous)’, ‘WaveForm’, ‘다음(Next)’ 버튼 세 개를 추가했습니다. ‘이전’과 ‘다음’ 버튼을 사용하면 기기에 저장된 다양한 애니메이션을 앞뒤로 순차적으로 전환할 수 있습니다.

WaveForm 버튼을 누르면 기기가 WaveForm 모드로 전환됩니다. 이 모드에서는 RGB 매트릭스가 내장 마이크가 포착한 소리에 실시간으로 반응하는 애니메이션 파형을 표시합니다.

이 Instructables에서는 케이스 설계, 전자 부품, 조립 과정, 코드를 포함하여 Project Gestalt의 전체 제작 과정을 다룰 것입니다.

그럼 시작해 봅시다!




STEP 1. 준비물


이번 프로젝트에 사용된 자재는 다음과 같습니다—

  1. 맞춤형 PCB
  2. 64x32 P3 HUB75 RGB 매트릭스 x 2
  3. 매트릭스 리본 커넥터
  4. 라즈베리 파이 PICO
  5. IP5306 전원 관리 IC
  6. 10 uF 커패시터
  7. 1 uH SMD 인덕터
  8. Type C 포트
  9. 0805 LED 표시등
  10. 10K 저항: 0805 패키지
  11. PCM이 장착된 3.7V 2600mAh 리튬 전지
  12. 12x12 크기의 푸시 버튼
  13. 6x6 크기 푸시 버튼
  14. 연결선




STEP 2. 『NieR: AUTOMATA』의 PODS


『NieR: AUTOMATA』의 PODS


『NieR: AUTOMATA』의 PODS


『NieR: AUTOMATA』의 PODS


이 프로젝트의 영감은 『NieR: Automata』의 ‘PODS’에서 비롯되었습니다.

『니어: 오토마타』를 잘 모르시는 분들을 위해 설명드리자면, ‘포드(Pods)’는 요르하(YoRHa) 병사들에게 일반적으로 배정되는 전술 지원 유닛입니다. 이들은 원거리 공격을 위한 다양한 무기를 갖추고 있으며, 사령부로부터 메시지를 전달하고, 부대 간 작전 정보를 교환하며, 임무 수행 중 전반적인 지원을 제공합니다.

게임의 대부분에서 포드는 단순히 명령을 따르는 감정이 없는 기계처럼 보입니다. 그들의 건조하고 지나치게 문자 그대로인 대화는 때때로 게임에서 가장 재미있는 순간들을 선사하기도 합니다. 하지만 『니어: 오토마타』의 막바지에 이르러, 팟의 역할은 훨씬 더 심오해집니다. 스포일러를 피하기 위해 자세한 내용은 생략하겠지만, 팟은 원래의 프로그래밍을 넘어선 무언가를 상징하기 시작하며, 이야기 속에서 예상치 못하게 의미 있는 존재가 됩니다.

『NieR: Automata』를 정말 좋아하는 사람으로서, 저는 꽤 오랫동안 포드에서 영감을 받은 나만의 장치를 만들어보고 싶었습니다. 작업대 위에 올려두고 실제로 사용할 수 있는 게임 기념품 같은 것이었죠.

흥미롭게도 RGB 매트릭스 디스플레이는 사실 원래 구상에는 포함되지 않았습니다. 이 프로젝트는 처음에는 단순히 나만의 ‘팟’과 유사한 장치를 설계하고 제작하려는 시도로 시작되었습니다. 두 개의 RGB 매트릭스를 통합하여 인터랙티브 디스플레이로 만들겠다는 아이디어는 설계 과정 중반에야 떠올랐습니다.

그 결과 탄생한 것이 바로 ‘프로젝트 게슈탈트(Project Gestalt)’입니다. 이 장치는 『니어: 오토마타』의 ‘포드’ 디자인과 64×64 RGB 매트릭스를 결합하여, 애니메이션, 게임 관련 영상, 무작위 패턴, 그리고 오디오에 반응하는 파형을 표시할 수 있습니다.




STEP 3. 디자인


디자인


디자인


디자인


디자인


디자인


디자인


디자인


디자인


디자인


이 프로젝트의 케이스 디자인에 있어, 제가 가장 큰 영감을 받은 것은 『NieR: Automata』에 등장하는 ‘팟(Pod)’, 특히 2B에게 배정된 ‘팟 042’였습니다. 모든 팟은 전반적으로 동일한 디자인을 공유하지만, 팟 042는 주황색 액센트 디테일로 인해 한눈에 알아볼 수 있습니다.

원작의 포드는 상당히 각진 디자인을 가지고 있는 반면, RGB 매트릭스는 거의 완전히 평평합니다. 이 때문에 정확한 복제본을 만들 수는 없었고, 대신 디스플레이를 수용하면서도 전체적인 외관을 살린 케이스를 디자인했습니다. 제가 꼭 살리고 싶었던 특징 중 하나는 두 개의 눈처럼 생긴 디테일이 있는 원통형 상단 부분이었는데, 이는 포드 디자인에서 가장 눈에 띄는 요소 중 하나입니다.

메탈릭한 외관을 구현하기 위해, 케이스의 대부분을 회색 PLA로 프린팅하고, Pod 042를 닮도록 별도의 주황색 액센트 부품을 추가할 계획이었습니다.

케이스는 본체와 후면 덮개라는 두 가지 주요 부품으로 구성됩니다. 본체에는 두 개의 RGB 매트릭스, 피코 드라이버 보드, 스위치 보드, 전원 스위치, 전면 그릴, 스위치 액추에이터가 모두 들어 있습니다. 후면 덮개는 케이스 후면을 덮어주며 M2 나사를 사용하여 고정됩니다.

Pod에서 영감을 받은 원통형 상단 부분은 본체와 후면 덮개 사이에 끼워져 있어, 별도의 고정 장치 없이도 제자리에 단단히 고정됩니다. 마지막으로, 오렌지색 액센트 부품을 초강력 접착제를 사용하여 케이스 전면에 부착함으로써 Pod에서 영감을 받은 전체적인 외관을 완성합니다.




STEP 4. 3D 프린팅 부품


3D 프린팅 부품


3D 프린팅 부품


3D 프린팅 부품


3D 프린팅 부품


케이스 디자인이 완료되자, 모든 부품을 메쉬 파일로 내보낸 후 제 Anycubic Kobra S1 3D 프린터에서 그레이 하이퍼 PLA와 오렌지 하이퍼 PLA를 사용하여 3D 프린팅했습니다.

본체, 후면 덮개, Pod에서 영감을 받은 실린더를 포함한 대형 구조 부품들은 그레이 하이퍼 PLA로, 그릴, 버튼 액추에이터, 전면 액센트 부품은 오렌지 하이퍼 PLA로 출력하여, Pod 042에서 영감을 받은 깔끔한 투톤 미학을 프로젝트에 더했습니다.

대부분의 출력 작업에는 다음 설정을 사용했습니다:

  1. 레이어 높이: 0.2 mm
  2. 채움률: 25%
  3. 노즐: 0.4 mm
  4. 지지대: 상단 간격 0.3 mm의 트리 지지대

이러한 설정은 출력 품질, 강도, 출력 시간 사이에서 적절한 균형을 제공했습니다.




STEP 5. 하드웨어 - 64x32 RGB 매트릭스


하드웨어 - 64x32 RGB 매트릭스


하드웨어 - 64x32 RGB 매트릭스


하드웨어 - 64x32 RGB 매트릭스


이 프로젝트에서는 64x32 RGB 매트릭스 패널 두 개를 사용하고 있으며, 각 패널은 64x32 격자로 배열된 2048개의 RGB LED로 구성되어 생동감 넘치는 텍스트, 이미지 및 애니메이션을 구현합니다.

이 패널은 HUB75 인터페이스를 통해 제어되며, 이 인터페이스는 RGB 핀, 주소 핀, 클럭 핀, 데이터 래치 및 출력 활성화 핀을 포함한 여러 제어 핀을 사용합니다.

HUB75 연결을 통해 픽셀 데이터 행을 시프트 레지스터로 이동시킨 다음, 디멀티플렉서를 사용하여 표시할 행을 선택함으로써 행-열 스캐닝 방식을 구현할 수 있습니다. HUB75 커넥터에는 RGB 채널, 어드레싱 핀(A, B, C, D), 클럭 신호(CLK), 래치 신호(LAT) 및 출력 활성화(OE) 핀이 포함되어 있습니다.

이 인터페이스를 사용하면 LED 매트릭스를 정밀하게 제어할 수 있어 텍스트, 이미지 및 애니메이션에 이상적인 부드럽고 밝은 디스플레이를 구현할 수 있습니다.

설정이 복잡해 보일 수 있지만, 이 방법은 고강도 디스플레이와 부드럽고 매끄러운 그래픽 전환을 보장합니다.

5V 전원을 사용하여 이 디스플레이를 구동할 수 있지만, 한 가지 주의할 점이 있습니다. 전류 소모량은 점등된 LED의 수와 밝기 수준에 따라 크게 달라질 수 있습니다. 패널 전체가 점등되면 상당한 전류가 소모될 수 있으므로, 최소 4~5A를 공급할 수 있는 전원 공급 장치를 사용하는 것이 좋습니다.

또한, 동봉된 IN 및 OUT 커넥터를 활용하면 여러 패널을 쌍으로 연결하여 체인을 형성할 수 있습니다. 다수의 패널을 연결할 때의 과제 중 하나는, 사용 중인 제어 솔루션(PICO 2)이 두 개 이상의 디스플레이에서 발생하는 추가 데이터 부하를 처리할 수 있는지 확인하는 것입니다.

현재 PICO 2를 사용하여 단일 패널로 두 개 이상의 디스플레이에 전원을 공급하고 있으며, 아무런 문제도 발생하지 않고 있습니다.

이 매트릭스는 Waveshare에서 제작했으며, 아래 위키 링크에서 매트릭스 보드에 대한 더 자세한 정보를 확인할 수 있습니다:

https://www.waveshare.com/wiki/RGB-Matrix-P3-64x32




STEP 6. PCB 설계 - PICO 드라이버


PCB 설계 - PICO 드라이버


PCB 설계 - PICO 드라이버


PCB 설계 - PICO 드라이버


PCB 설계 - PICO 드라이버


이 프로젝트를 위해 설계된 첫 번째 PCB는 ‘피코 드라이버 보드(Pico Driver Board)’로, 프로젝트 게슈탈트(Project Gestalt)의 메인 컨트롤러 역할을 합니다. 이 보드는 표준 HUB75(CON16) 커넥터를 통해 라즈베리 파이 피코 2(Raspberry Pi Pico 2)와 RGB 매트릭스를 연결합니다.

HUB75 신호는 다음과 같이 Pico에 연결됩니다:

  1. A → GPIO19
  2. B → GPIO16
  3. C → GPIO18
  4. D → GPIO20
  5. E는 GPIO22에
  6. CLK → GPIO11
  7. LAT/STB → GPIO12
  8. OE를 GPIO13에 연결
  9. R1을 GPIO2에 연결
  10. G1 → GPIO3
  11. B1 → GPIO4
  12. R2 → GPIO5
  13. G2를 GPIO8에 연결
  14. B2를 GPIO9에 연결합니다.

또한 CON2 커넥터가 포함되어 있으며, 이 커넥터는 Pico의 5V 및 GND 핀에 연결되어 드라이버 보드에 전원을 공급합니다.

또한, 여러 GPIO 핀을 분배하는 CON6 커넥터를 추가했습니다. 이 커넥터는 나중에 터치 버튼과 마이크 모듈을 연결하는 데 사용될 것입니다.

이 PCB에는 DFPlayer Mini 오디오 모듈용 풋프린트도 포함되어 있습니다. 이 프로젝트의 버전 1에서는 사용되지 않지만, 향후 개정판에서 오디오 기능을 쉽게 추가할 수 있도록 보드에 포함시키기로 결정했습니다.

회로도가 완성된 후, 3D 모델의 치수를 기반으로 PCB 외곽선을 생성했습니다. 장착 구멍을 배치한 뒤, 모든 부품과 커넥터의 위치를 정하고 기판 배선 작업을 완료했습니다.




STEP 7. PCB 설계 - 스위치 보드


PCB 설계 - 스위치 보드


PCB 설계 - 스위치 보드


PCB 설계 - 스위치 보드


PCB 설계 - 스위치 보드


다음으로, 12×12 mm 크기의 촉감식 푸시 버튼 3개와 마이크 모듈이 장착된 스위치 보드를 설계했습니다.

세 개의 푸시 버튼은 한 단자가 GND에 연결되어 있고, 나머지 단자들은 각각 CON6 커넥터로 연결되어 있습니다. 이 커넥터는 Pico 드라이버 보드와 직접 연동되어 버튼 신호를 전달합니다.

이 보드에는 MAX9814 마이크 모듈도 포함되어 있습니다. 이 모듈의 출력 신호와 VCC 및 GND 연결은 동일한 CON6 커넥터를 통해 연결되어, 라즈베리 파이 피코가 웨이브폼(WaveForm) 모드를 위해 주변 오디오를 읽을 수 있게 합니다.

Pico 드라이버 보드와 마찬가지로, PCB 외형은 3D 모델의 치수를 사용하여 생성되었습니다. 스위치와 커넥터의 위치를 정한 후, 트레이스를 배선하여 보드 설계를 완료했습니다.




STEP 8. PCB 설계 - 전원 보드


PCB 설계 - 전원 보드


PCB 설계 - 전원 보드


PCB 설계 - 전원 보드


PCB 설계 - 전원 보드


PCB 설계 - 전원 보드


이 프로젝트에 사용된 세 번째 PCB는 전체 시스템의 주 전원 공급원 역할을 하는 전원 보드입니다.

3.7V 리튬 이온/리튬 폴리머 셀은 IP5306 전원 관리 IC에 연결되어 있으며, 이 IC는 배터리 전압을 승압하여 RGB 매트릭스와 피코 드라이버 보드 모두에 전원을 공급할 수 있는 안정적인 5V, 2A 출력을 제공합니다.

또한 이 보드에는 IP5306의 KEY 핀에 연결된 순간 누름 버튼이 포함되어 있습니다. 이 버튼을 누르면 출력이 활성화되어 전체 장치에 전원이 공급됩니다. 케이스를 열지 않고도 편리하게 배터리를 충전할 수 있도록 USB Type-C 커넥터가 제공됩니다.

앞서 소개한 두 개의 PCB와 마찬가지로, 이 보드의 외형은 Fusion 360 케이스 모델의 치수를 바탕으로 설계되었습니다. 기계적 제약 조건이 확정된 후, 모든 부품을 제자리에 배치하고 PCB 배선 작업을 완료했습니다.




STEP 9. NextPCB PCB 서비스


NextPCB PCB 서비스


NextPCB PCB 서비스


NextPCB PCB 서비스


NextPCB PCB 서비스


PCB 설계를 완료한 후, 세 개의 PCB 모두에 대한 거버(Gerber) 데이터를 HQ NextPCB로 전송했고, 세 개 모두에 대해 녹색 솔더 마스크 기판과 흰색 실크스크린을 적용한 제품을 주문했습니다.

주문을 한 후 일주일 만에 PCB를 수령했으며, PCB 품질은 꽤 훌륭했습니다.

또한, 여러 프로젝트를 진행하는 동안 큰 도움이 되었던 HQDFM을 소개해 드리고 싶습니다. Huaqiu의 사내 엔지니어들이 개발한 무료 제조 설계(Design for Manufacturing) 소프트웨어인 HQDFM은 PCB 설계자들이 설계를 시각화하고 검증하는 방식에 혁명을 일으켰습니다.

NextPCB의 ‘Accelerator’ 캠페인을 활용하여 혁신적인 프로젝트를 위해 RP2040 기반 조립 완료 PCB 2개를 무료로 받아보세요.

https://www.nextpcb.com/blog/rp2040-free-pcba-prototypes-nextpcb-accelerator

이 혜택에는 물류비를 포함한 모든 비용이 포함되어 있어, 여러분의 아이디어를 현실로 구현하는 과정을 더욱 쉽고 경제적으로 만들어 드립니다. SMT 서비스는 비용이 많이 들 수 있지만, NextPCB가 그 장벽을 극복할 수 있도록 도와드립니다. 관련 프로젝트를 공유하기만 하면 나머지는 NextPCB가 처리해 드립니다. 기술 창작물을 한 단계 발전시킬 수 있는 이 놀라운 기회를 놓치지 마세요!




STEP 10. HQDFM: 무료 온라인 거버 뷰어 및 DFM 분석 도구


HQDFM: 무료 온라인 거버 뷰어 및 DFM 분석 도구


또한, NextPCB는 자체 Gerber 뷰어와 DFM 분석 소프트웨어를 제공합니다.

제 디자인은 그들의 HQDFM 소프트웨어(DFM) 서비스로 개선됩니다. 제조업체로부터 DFM 보고서를 기다리는 것이 귀찮기 때문에, HQDFM 는 사전 이벤트 자체 점검을 수행하는 가장 효율적인 방법입니다.

온라인 Gerber 뷰어에서 제가 보는 것은 이렇습니다. 빠르게 살펴보기에 괜찮지만, 완전히 명확하지는 않습니다. 전체 기능—예를 들어 PCBA에 대한 상세 DFM 분석—을 사용하려면 데스크탑 소프트웨어를 다운로드해야 합니다. 웹 버전은 기본 DFM 보고서만 제공합니다.

종합적인 제조를 위한 설계(DFM) 분석 기능을 갖춘 HQDFM은 무료이며, 정교한 온라인 PCB Gerber 파일 뷰어입니다.

15년 이상의 산업 경험을 바탕으로 고급 제조 공정에 대한 귀중한 통찰력을 제공합니다. 예산 친화적인 가격으로 신뢰할 수 있는 PCB 서비스를 찾고 있다면, HQ NextPCB는 꼭 확인해볼 가치가 있습니다.




STEP 11. PCB 조립 - PICO 드라이버


PCB 조립 - PICO 드라이버


PCB 조립 - PICO 드라이버


PCB 조립 - PICO 드라이버


PCB 조립 - PICO 드라이버


PCB 조립 - PICO 드라이버


  1. 피코 드라이버 보드 조립 과정은 솔더 페이스트 도포 바늘을 사용하여 모든 피코 패드에 솔더 페이스트를 도포하는 것으로 시작합니다. 여기서는 융점이 200°C인 Sn/Pb 63/37 솔더 페이스트를 사용하고 있습니다.
  2. 다음으로, 라즈베리 파이 피코를 PCB 위에 픽 앤 플레이스하여 모든 핀이 해당 패드와 정확히 정렬되도록 합니다.
  3. 그런 다음 PCB를 리플로우 핫플레이트 위에 올립니다. 여기서는 피코 드라이버 보드 조립에 안성맞춤인 소형 리플로우 핫플레이트인 Miniware MH50을 사용하고 있습니다.
  4. 마지막으로, HUB75 커넥터 패드에 CON8 수 헤더 커넥터 두 개를 장착합니다. 보드를 뒤집은 후 납땜 인두를 사용하여 헤더 핀을 제자리에 납땜함으로써 Pico 드라이버 보드의 조립을 완료합니다.




STEP 12. PCB 조립 - 스위치 보드


PCB 조립 - 스위치 보드


PCB 조립 - 스위치 보드


PCB 조립 - 스위치 보드


PCB 조립 - 스위치 보드


  1. 다음은 스위치 보드의 PCB 조립 공정으로, 먼저 MAX9814 마이크 모듈과 세 개의 푸시 버튼을 각각의 위치에 배치하는 것으로 시작합니다.
  2. 그런 다음 보드를 뒤집고, 납땜 인두를 사용하여 모든 리드를 납땜하여 각 부품을 제자리에 단단히 고정합니다.




STEP 13. PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


PCB 조립 - 전원 보드


  1. 먼저, 앞서 사용했던 것과 동일한 솔더 페이스트 디스펜서를 사용하여 모든 부품 패드에 솔더 페이스트를 도포합니다.
  2. 다음으로, 모든 SMD 부품을 집어 각각의 위치에 배치합니다.
  3. 그런 다음 PCB 전체를 리플로우 핫플레이트 위에 올립니다. 이번에는 파워 보드가 이전 PCB들보다 크기가 더 크기 때문에 약간 더 큰 핫플레이트를 사용합니다.
  4. 리플로우 공정이 완료되고 모든 SMD 부품이 고정되면, 푸시 버튼을 장착한 다음 USB Type-C 커넥터를 장착합니다.
  5. 그런 다음 보드를 뒤집고, 납땜 인두를 사용하여 스루홀 부품의 리드를 납땜하여 제자리에 고정합니다.
  6. 다음으로, 3.7V, 2600mAh 리튬이온 셀의 양극을 파워 보드의 B+ 단자에, 음극을 B- 단자에 납땜 인두를 사용하여 연결합니다.

모든 것이 정상적으로 작동하는지 확인하기 위해 전원 버튼을 누릅니다. 상태 LED가 켜지면 보드가 전원이 켜진 것을 의미합니다. 그런 다음 멀티미터를 사용하여 출력 전압을 측정해 안정적인 5V가 출력되는지 확인하고, 파워 보드가 예상대로 작동하고 있음을 확인합니다.




STEP 14. PICO 드라이버 및 매트릭스 어셈블리


PICO 드라이버 및 매트릭스 어셈블리


PICO 드라이버 및 매트릭스 어셈블리


PICO 드라이버 및 매트릭스 어셈블리


  1. 먼저 납땜 인두를 사용하여 RGB 매트릭스의 VCC 및 GND 단자를 Pico 드라이버 보드의 5V 및 GND 출력 단자에 연결합니다.
  2. 다음으로, 16핀 리본 케이블을 사용하여 매트릭스의 HUB75 인터페이스를 Pico 드라이버 보드의 CON16 커넥터에 연결합니다.




STEP 15. 매트릭스 데모


매트릭스 데모


매트릭스 데모


매트릭스 데모


그런 다음, 팩맨에서 영감을 받아 발이 움직이고 RGB 색상이 끊임없이 변하는 유령 ‘사이버 고스트(Cyber Ghost)’를 표시하는 아래의 데모 코드를 업로드합니다.

다음은 전체 코드입니다.

#include <Adafruit_Protomatter.h>
#include <math.h>

// ── 핀 (라즈베리 파이 피코 GPIO) ─────────────────────────────────
#define R1 2
#define G1 3
#define B1 4
#define R2 5
#define G2 6 // GPIO 6 사용
#define B2 9
#define PIN_A 10
#define PIN_B 16
#define PIN_C 18
#define PIN_D 20
#define CLK 11
#define LAT 12
#define OE 13

// ── 단일 64x32 패널의 해상도 ───────────────────────────
#define W 64
#define H 32

uint8_t rgbPins[] = { R1, G1, B1, R2, G2, B2 };
uint8_t addrPins[] = { PIN_A, PIN_B, PIN_C, PIN_D };

// ── 생성자 ───────────────────────────────────────────────────
// 비트 심도 = 3 (최대 재생 속도, 깜빡임 없음!)
// 더블 버퍼 = true
// 타일링 = 1 (단일 매트릭스)
Adafruit_Protomatter matrix(
W, 3, 1, rgbPins,
4, addrPins,
CLK, LAT, OE,
true,
1
);

// ── 전역 변수 및 타이밍 ─────────────────────────────────────
uint32_t frameCount = 0;
float ghostHue = 360.0f; // 빨간색(360도)에서 시작

// 빠른 RGB565 색상 헬퍼
inline uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
return matrix.color565(r, g, b);
}

// ── 부드러운 HSV-RGB 변환기 ───────────────────────────────────
// 색상각(0.0~360.0)을 부드러운 RGB565 색상으로 변환합니다.
// 색상(Hue) 값을 감소시키면 순환합니다: 빨강 -> 파랑 -> 초록 -> 빨강
uint16_t getSmoothHue(float h) {
while (h < 0.0f) h += 360.0f;
while (h >= 360.0f) h -= 360.0f;
float c = 1.0f; // 최대 채도 및 명도
float x = c * (1.0f - fabsf(fmodf(h / 60.0f, 2.0f) - 1.0f));
float r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }

return rgb((uint8_t)(r * 255), (uint8_t)(g * 255), (uint8_t)(b * 255));
}

// ══════════════════════════════════════════════════════════════════
// SCENE: 크기가 조정된 무지개 사이버 고스트 (64x32용 부드러운 사인 물리 엔진)
// ══════════════════════════════════════════════════════════════════
void drawPacGhost() {
matrix.fillScreen(0);
float t = frameCount * 0.06f;
// 1. 색상 변화: 색조를 뒤로 부드럽게 순환 (빨강 -> 파랑 -> 초록 -> 빨강)
ghostHue -= 0.6f; // 이 수치를 조정하여 색상 순환 속도를 빠르게 또는 느리게 조절하세요!
if (ghostHue < 0.0f) ghostHue += 360.0f;
uint16_t GHOST_COLOR = getSmoothHue(ghostHue);
uint16_t EYE_WHITE = rgb(240, 240, 240);
uint16_t PUPIL_BLUE = rgb(0, 50, 255);
// 2. 공간 이동: 중심 X축은 좌우로 미끄러지고, 중심 Y축은 부드럽게 떠다님
float base_cx = 32.0f + sinf(t * 1.2f) * 18.0f;
float hover_y = sinf(t * 3.0f) * 1.5f;
// 높이 32px에 맞춰진 본체 바운딩 박스 렌더링
for (int y = 4; y < 28; y++) {
for (int x = base_cx - 10; x < base_cx + 10; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;

float relX = x - base_cx;
float relY = (y - 5) + hover_y;

float headRadius = 8.0f;
float bodyHeight = 12.0f;
bool drawPixel = false;
// 반원형 돔 머리
if (relY < headRadius) {
float dx = relX;
float dy = relY - headRadius;
if (sqrtf(dx*dx + dy*dy) < headRadius) {
drawPixel = true;
}
}
// 몸통 중앙을 채우기
else if (relY >= headRadius && relY < bodyHeight + headRadius) {
if (relX >= -8.0f && relX <= 8.0f) {
drawPixel = true;
}
}
// 물결치는 발 애니메이션
else if (relY >= bodyHeight + headRadius) {
float footRipple = 1.5f * sinf(relX * 0.9f - t * 4.0f);
if (relY < (bodyHeight + headRadius + 3.0f + footRipple) && relX >= -8.0f && relX <= 8.0f) {
drawPixel = true;
}
}
if (drawPixel) {
matrix.drawPixel(x, y, GHOST_COLOR);
}
}
}
// 3. 동적 시선 추적
float lookX = sinf(t * 1.2f) * 2.0f;
float lookY = cosf(t * 1.5f) * 1.0f;
auto drawEye = [&](float ex, float ey) {
for (int y = ey - 3; y < ey + 3; y++) {
for (int x = ex - 2; x < ex + 2; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;
float dx = x - ex;
float dy = y - (ey + hover_y);
if((dx*dx / 4.0f) + (dy*dy / 9.0f) < 1.0f) {
matrix.drawPixel(x, y, EYE_WHITE);
}
}
}
// 파란색 동공
int px = ex + lookX;
int py = (ey + hover_y) + lookY;
matrix.fillRect(px, py, 2, 2, PUPIL_BLUE);
};

drawEye(base_cx - 4.0f, 11.0f);
drawEye(base_cx + 4.0f, 11.0f);
}

// ── 설정 및 루프 ───────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
ProtomatterStatus s = matrix.begin();
if (s != PROTOMATTER_OK) {
Serial.print("프로토매터 초기화 오류: ");
Serial.println((int)s);
while(1);
}
}

void loop() {
drawPacGhost();
matrix.show();
frameCount++;
// 15ms 지연으로 약 60 FPS의 매우 안정적인 애니메이션 루프가 생성됩니다
delay(15);
}




STEP 16. 이중 매트릭스 어셈블리


이중 매트릭스 어셈블리


이중 매트릭스 어셈블리


단일 매트릭스 구성을 테스트한 후, 두 번째 RGB 매트릭스의 VCC 및 GND 라인을 첫 번째 매트릭스의 해당 라인과 병렬로 연결하여 추가했습니다.

다음으로, 약간 더 긴 16핀 리본 케이블을 사용하여 첫 번째 매트릭스의 HUB75 DOUT 커넥터를 두 번째 매트릭스의 HUB75 DIN 커넥터에 연결했습니다. 이를 통해 두 개의 64×32 매트릭스를 데이지 체인 방식으로 연결하여, 하나의 64×64 RGB 매트릭스 디스플레이로 작동할 수 있게 되었습니다.

본문이 길어 아래 덧글 3개로 계속됩니다.


원문: https://www.instructables.com/Project-Gestalt/

좋아요 0
게시판

본문 계속 1/3




STEP 17. 듀얼 매트릭스 데모


듀얼 매트릭스 데모


듀얼 매트릭스 데모


단일 매트릭스 데모 코드와 마찬가지로, 총 해상도가 64×64 픽셀인 듀얼 매트릭스 구성에도 동일한 Cyber-Ghost 애니메이션을 준비했습니다.

아래 코드를 Raspberry Pi Pico에 업로드했더니, 두 RGB 매트릭스가 하나의 64×64 디스플레이로 함께 작동하며 설정이 완벽하게 작동했습니다.

업데이트된 코드

#include <Adafruit_Protomatter.h>
#include <math.h>

// ── 핀 (라즈베리 파이 피코 GPIO) ─────────────────────────────────
#define R1 2
#define G1 3
#define B1 4
#define R2 5
#define G2 6 // GPIO 6 확인됨
#define B2 9
#define PIN_A 10
#define PIN_B 16
#define PIN_C 18
#define PIN_D 20
#define CLK 11
#define LAT 12
#define OE 13

// ── 2개의 64x32 패널을 연결한 경우의 해상도 (총 64x64) ─────────
#define W 64
#define H 64

uint8_t rgbPins[] = { R1, G1, B1, R2, G2, B2 };
uint8_t addrPins[] = { PIN_A, PIN_B, PIN_C, PIN_D };

// ── 생성자 ───────────────────────────────────────────────────
// 비트 심도 = 3 (최대 재생 속도, 깜빡임 없음!)
// 더블 버퍼 = true
// 타일링 = 2 (연쇄 연결된 두 번째 패널이 거꾸로 표시되면 -2로 변경)
Adafruit_Protomatter matrix(
W, 3, 1, rgbPins,
4, addrPins,
CLK, LAT, OE,
true,
2
);

// ── 전역 변수 및 타이밍 ─────────────────────────────────────
uint32_t frameCount = 0;

// 고속 RGB565 색상 헬퍼
inline uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
return matrix.color565(r, g, b);
}

// ══════════════════════════════════════════════════════════════════
// SCENE: 스케일링된 레인보우 사이버 고스트 (64x64용 부드러운 사인 물리 엔진)
// ══════════════════════════════════════════════════════════════════
void drawPacGhost() {
matrix.fillScreen(0);
float t = frameCount * 0.05f;
// ── 초고속 정수 색상 순환: 빨강 -> 보라 -> 파랑 -> 초록 -> 빨강 ──
// 순수한 8비트 정수 연산을 사용하면 CPU 부동소수점 연산 지연을 방지하고 상단에서 하단으로 스캔되는 지연을 없앨 수 있습니다!
uint8_t wheelPos = (frameCount >> 1) & 0xFF; // 색상 전환 속도를 늦추려면 (>> 1)을 (>> 2)로 변경하세요
uint8_t r, g, b;
if (wheelPos < 85) {
r = 255 - wheelPos * 3;
g = 0;
b = wheelPos * 3;
} else if (wheelPos < 170) {
wheelPos -= 85;
r = 0;
g = wheelPos * 3;
b = 255 - wheelPos * 3;
} else {
wheelPos -= 170;
r = wheelPos * 3;
g = 255 - wheelPos * 3;
b = 0;
}
uint16_t GHOST_COLOR = rgb(r, g, b);
uint16_t EYE_WHITE = rgb(240, 240, 240);
uint16_t PUPIL_BLUE = rgb(0, 50, 255);
// ── 공간 이동: 중앙 X축은 좌우로 드리프트하고, 중앙 Y축은 부드럽게 떠다님 ──
float base_cx = 32.0f + sinf(t * 1.2f) * 16.0f;
float hover_y = sinf(t * 3.0f) * 2.5f;
// 64px 세로 캔버스에 맞춰 크기가 조정된 본체 바운딩 박스 렌더링
for (int y = 8; y < 58; y++) {
for (int x = base_cx - 18; x < base_cx + 18; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;

float relX = x - base_cx;
float relY = (y - 12) + hover_y;

// 64x64 그리드를 채우기 위해 비율을 확대
float headRadius = 15.0f;
float bodyHeight = 24.0f;
bool drawPixel = false;
// 1. 반원형 돔 머리
if (relY < headRadius) {
float dx = relX;
float dy = relY - headRadius;
if (sqrtf(dx*dx + dy*dy) < headRadius) {
drawPixel = true;
}
}
// 2. 몸통 중앙 부분 채우기
else if (relY >= headRadius && relY < bodyHeight + headRadius) {
if (relX >= -15.0f && relX <= 15.0f) {
drawPixel = true;
}
}
// 3. 발 부분의 물결 효과 (하단 가장자리를 따라 유동적인 사인파)
else if (relY >= bodyHeight + headRadius) {
float footRipple = 2.5f * sinf(relX * 0.7f - t * 4.0f);
if (relY < (bodyHeight + headRadius + 4.0f + footRipple) && relX >= -15.0f && relX <= 15.0f) {
drawPixel = true;
}
}
if (drawPixel) {
matrix.drawPixel(x, y, GHOST_COLOR);
}
}
}
// ── 동적 시선 추적 (유령이 움직일 때 시선이 자연스럽게 이동함) ──
float lookX = sinf(t * 1.2f) * 3.0f;
float lookY = cosf(t * 1.5f) * 1.5f;
auto drawEye = [&](float ex, float ey) {
// 세로로 긴 타원형 눈 (6x10 픽셀)
for (int y = ey - 5; y < ey + 5; y++) {
for (int x = ex - 3; x < ex + 3; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;
float dx = x - ex;
float dy = y - (ey + hover_y);
if((dx*dx / 9.0f) + (dy*dy / 25.0f) < 1.0f) {
matrix.drawPixel(x, y, EYE_WHITE);
}
}
}
// 2x2 파란색 동공
int px = ex + lookX;
int py = (ey + hover_y) + lookY;
matrix.fillRect(px - 1, py - 1, 2, 2, PUPIL_BLUE);
};

// 더 큰 머리 위에 왼쪽 눈과 오른쪽 눈을 대칭으로 배치
drawEye(base_cx - 6.0f, 22.0f);
drawEye(base_cx + 6.0f, 22.0f);
}

// ── 설정 및 루프 ───────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
ProtomatterStatus s = matrix.begin();
if (s != PROTOMATTER_OK) {
Serial.print("프로토매터 초기화 오류: ");
Serial.println((int)s);
while(1);
}
}

void loop() {
drawPacGhost();
matrix.show();
frameCount++;
// 15ms 지연으로 약 60 FPS의 매우 안정적인 애니메이션 루프가 생성됩니다
delay(15);
}




STEP 18. 배선 - PICO 드라이버 및 스위치 보드


배선 - PICO 드라이버 및 스위치 보드


배선 - PICO 드라이버 및 스위치 보드


  1. 먼저 피코 드라이버 보드를 RGB 매트릭스에서 분리하고 스위치 보드에 연결합니다.
  2. 먼저 피코 드라이버 보드의 GND를 스위치 보드의 GND에 연결한 다음, 피코 드라이버 보드의 5V 출력을 스위치 보드의 VCC 입력에 연결합니다.
  3. 다음으로, 스위치 1(PREV)을 GPIO0에, 스위치 2(NEXT)를 GPIO14에, 스위치 3(WAVEFORM MODE)을 GPIO1에 연결합니다. 마지막으로, 스위치 보드의 마이크 출력 핀을 Pico의 GPIO28에 연결합니다.

이 모든 연결에는 단일 심 은도금 구리선을 사용합니다.




STEP 19. 배선 - 전원 보드가 포함된 PICO 드라이버 스위치 보드


배선 - 전원 보드가 포함된 PICO 드라이버 스위치 보드


파워 보드의 5V 및 GND 출력은 두 개의 연결선을 사용하여 피코 드라이버 보드의 5V 및 GND 단자에 연결됩니다.

본문 계속 2/3




STEP 20. 코드


매트릭스 조립 과정을 시작하기 전에 아래의 메인 코드를 라즈베리 파이 피코에 업로드했습니다.

#include <Adafruit_Protomatter.h>
#include <math.h>

// ── Pins (Raspberry Pi Pico GPIO) ─────────────────────────────────
#define R1 2
#define G1 3
#define B1 4
#define R2 5
#define G2 6
#define B2 9
#define PIN_A 10
#define PIN_B 16
#define PIN_C 18
#define PIN_D 20
#define CLK 11
#define LAT 12
#define OE 13

// ── Buttons ───────────────────────────────────────────────────────
#define BTN_PREV 0
#define BTN_NEXT 14
#define BTN_WAVE 1

// ── Resolution for TWO Chained 64x32 Panels (64x64 total) ─────────
#define W 64
#define H 64

uint8_t rgbPins[] = { R1, G1, B1, R2, G2, B2 };
uint8_t addrPins[] = { PIN_A, PIN_B, PIN_C, PIN_D };

Adafruit_Protomatter matrix(
W, 3, 1, rgbPins, 4, addrPins, CLK, LAT, OE, true, 2
);

// ── Global Variables & Timing ─────────────────────────────────────
uint32_t frameCount = 0;
int currentAnim = 0;
const int NUM_ANIMS = 9;

unsigned long previousMillis = 0;
const int frameInterval = 16;

bool lastBtnPrev = HIGH;
bool lastBtnNext = HIGH;
bool lastBtnWave = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

// ── Audio / Waveform Settings ─────────────────────────────────────
const int MIC_PIN = 28; // Using GPIO 28 (ADC2)
const int OVERSAMP = 128;
const int X_STEP = 2;
const int NUM_POINTS = W / X_STEP;
const float MAX_WAVE_FRACTION = 0.5f;

float rmsPeak = 200.0f;
float loudSmooth = 0.0f;
bool noiseInit = false;
float noiseFloor = 100.0f;
const float LOUD_GATE = 0.08f;

float waveY[NUM_POINTS];
bool waveMode = false;

inline uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
return matrix.color565(r, g, b);
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 0: The Original Cyber-Ghost
// ══════════════════════════════════════════════════════════════════
void drawPacGhost() {
matrix.fillScreen(0);
float t = frameCount * 0.05f;
uint8_t wheelPos = (frameCount >> 1) & 0xFF;
uint8_t r, g, b;
if (wheelPos < 85) { r = 255 - wheelPos * 3; g = 0; b = wheelPos * 3; }
else if (wheelPos < 170) { wheelPos -= 85; r = 0; g = wheelPos * 3; b = 255 - wheelPos * 3; }
else { wheelPos -= 170; r = wheelPos * 3; g = 255 - wheelPos * 3; b = 0; }
uint16_t GHOST_COLOR = rgb(r, g, b);
float base_cx = 32.0f + sinf(t * 1.2f) * 16.0f;
float hover_y = sinf(t * 3.0f) * 2.5f;
for (int y = 8; y < 58; y++) {
for (int x = base_cx - 18; x < base_cx + 18; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;
float relX = x - base_cx; float relY = (y - 12) + hover_y;
bool drawPixel = false;
if (relY < 15.0f) {
if (sqrtf(relX*relX + (relY - 15.0f)*(relY - 15.0f)) < 15.0f) drawPixel = true;
} else if (relY >= 15.0f && relY < 39.0f) {
if (relX >= -15.0f && relX <= 15.0f) drawPixel = true;
} else if (relY >= 39.0f) {
float footRipple = 2.5f * sinf(relX * 0.7f - t * 4.0f);
if (relY < (43.0f + footRipple) && relX >= -15.0f && relX <= 15.0f) drawPixel = true;
}
if (drawPixel) matrix.drawPixel(x, y, GHOST_COLOR);
}
}
float lookX = sinf(t * 1.2f) * 3.0f; float lookY = cosf(t * 1.5f) * 1.5f;
auto drawEye = [&](float ex, float ey) {
for (int y = ey - 5; y < ey + 5; y++) {
for (int x = ex - 3; x < ex + 3; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;
float dx = x - ex; float dy = y - (ey + hover_y);
if((dx*dx / 9.0f) + (dy*dy / 25.0f) < 1.0f) matrix.drawPixel(x, y, rgb(240, 240, 240));
}
}
matrix.fillRect(ex + lookX - 1, (ey + hover_y) + lookY - 1, 2, 2, rgb(0, 50, 255));
};
drawEye(base_cx - 6.0f, 22.0f); drawEye(base_cx + 6.0f, 22.0f);
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 1: Cyberpunk 2077 Relic Glitch
// ══════════════════════════════════════════════════════════════════
void drawCyberpunkRelic() {
matrix.fillScreen(rgb(10, 0, 15)); // Dark corporate background
uint16_t neonCyan = rgb(0, 255, 255);
uint16_t neonYellow = rgb(255, 240, 0);
uint16_t neonRed = rgb(255, 0, 50);

// Intentional X-axis tearing/glitching for the entire logo
int glitchX = (random(100) > 85) ? random(-6, 6) : 0;
// Base coordinates for the logo
int x = 16 + glitchX;
int y = 8;

// ── Draw the Custom Relic Logo Geometry ──
// Outer Frame
matrix.fillRect(x, y, 32, 6, neonRed); // Top horizontal
matrix.fillRect(x + 26, y, 6, 32, neonRed); // Right vertical
matrix.fillRect(x + 14, y + 26, 18, 6, neonRed); // Bottom horizontal (partial)
matrix.fillRect(x, y, 6, 22, neonRed); // Left vertical
// Inner 'R' Mechanism
matrix.fillRect(x, y + 12, 16, 6, neonRed); // Middle horizontal
matrix.fillRect(x + 14, y + 12, 6, 9, neonRed); // Inner right vertical
// The iconic 45-degree angle cut
matrix.fillRect(x, y + 22, 10, 6, neonRed); // Bottom-left inward turn
for(int i = 0; i < 6; i++) {
matrix.drawLine(x + 8 + i, y + 27, x + 16 + i, y + 19, neonRed); // Thick diagonal line
}

// Draw "RELIC" Text below the logo
matrix.setCursor(17 + glitchX, 44);
matrix.setTextColor(neonRed);
matrix.print("RELIC");

// Glitching core data block inside the logo
matrix.fillRect(x + 12, y + 16, 4, 4, neonYellow);

// ── Glitch & Artifact Effects ──
// Digital barcode / hacking lines cutting across
for(int i = 0; i < 7; i++) {
int lineY = random(0, 64);
matrix.drawFastHLine(random(0, 32), lineY, random(10, 45), neonCyan);
}
// Occasional full-screen EMP tear (Yellow flash with a black void)
if (random(100) > 92) {
int tearY = random(10, 50);
matrix.fillRect(0, tearY, 64, random(2, 6), neonYellow);
matrix.fillRect(0, tearY + 2, 64, random(2, 5), 0); // Black tear slicing the screen
}
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 2: Retro Doom Fire (Integer Cellular Automata)
// ══════════════════════════════════════════════════════════════════
uint8_t firePixels[W * (H + 1)]; // 1D array for speed
void drawRetroFire() {
// Feed the bottom row with random bright intensity
for (int x = 0; x < W; x++) {
firePixels[(H - 1) * W + x] = random(160, 255);
}
// Propagate upwards
for (int y = 0; y < H - 1; y++) {
for (int x = 0; x < W; x++) {
int src = (y + 1) * W + x;
int decay = random(0, 3);
int dstX = x - decay + 1;
if (dstX < 0) dstX = 0; if (dstX > W - 1) dstX = W - 1;
int dst = y * W + dstX;
int val = firePixels[src] - decay * 2;
firePixels[dst] = (val > 0) ? val : 0;
// Fast color mapping (Black -> Red -> Orange -> Yellow -> White)
uint8_t intensity = firePixels[dst];
uint8_t r = (intensity > 128) ? 255 : intensity * 2;
uint8_t g = (intensity > 128) ? (intensity - 128) * 2 : 0;
uint8_t b = (intensity > 192) ? (intensity - 192) * 4 : 0;
matrix.drawPixel(x, y, rgb(r, g, b));
}
}
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 3: XOR Cyber-Fractal (Fast Integer Hypnosis)
// ══════════════════════════════════════════════════════════════════
void drawXORFractal() {
matrix.fillScreen(0);
uint16_t t = frameCount * 2;
for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
// Pure mathematical bitwise alien pattern
uint8_t c = ((x * 4) ^ (y * 4)) - t;
uint8_t r = (c * 2) % 255;
uint8_t g = (c * 4) % 255;
uint8_t b = 255 - c;
matrix.drawPixel(x, y, rgb(r, g, b));
}
}
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 4: Synthwave Outrun Grid
// ══════════════════════════════════════════════════════════════════
void drawSynthwave() {
matrix.fillScreen(rgb(5, 0, 15)); // Dark retro-purple sky

// 1. Synthwave Sun (with scrolling scanline cutouts)
int sunY = 24;
for (int y = 10; y < 38; y++) {
int slice = (y - (frameCount >> 1)) % 6;
if (y > 24 && slice < 2) continue; // Scanline gaps
int dy = y - sunY;
if (dy * dy <= 196) { // Circle radius 14 (14^2 = 196)
int dx = sqrt(196 - dy * dy);
uint8_t g = (38 - y) * 7; // Gradient: Yellow top, Red bottom
matrix.drawFastHLine(32 - dx, y, dx * 2, rgb(255, g, 0));
}
}

// 2. Glowing Cyan Horizon
matrix.drawFastHLine(0, 38, W, rgb(0, 255, 255));

// 3. Perspective Grid (Floor)
// Radiating vertical lines
for (int x = -100; x < 164; x += 18) {
matrix.drawLine(32, 38, x, 63, rgb(200, 0, 255));
}
// Moving horizontal perspective lines (speed controlled by frameCount offset)
int offset = (frameCount >> 1) % 6;
for (int y = 39; y < H; y++) {
int depth = y - 38;
if ((depth + offset) % (depth / 3 + 2) == 0) {
matrix.drawFastHLine(0, y, W, rgb(255, 0, 255));
}
}
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 5: Neon Qix Trails (Mystify)
// ══════════════════════════════════════════════════════════════════
static float qx[4] = {10, 54, 10, 54};
static float qy[4] = {10, 10, 54, 54};
static float qvx[4] = {1.5, -1.2, 1.3, -1.6};
static float qvy[4] = {1.2, 1.7, -1.1, 1.4};

#define TAIL 8
static int hx[4][TAIL] = {0};
static int hy[4][TAIL] = {0};

void drawNeonQix() {
matrix.fillScreen(0);
// Shift trail history
for(int i = 0; i < 4; i++) {
for(int t = TAIL - 1; t > 0; t--) {
hx[i][t] = hx[i][t-1];
hy[i][t] = hy[i][t-1];
}
hx[i][0] = (int)qx[i];
hy[i][0] = (int)qy[i];
}

// Move head points and bounce off edges
for(int i = 0; i < 4; i++) {
qx[i] += qvx[i];
qy[i] += qvy[i];
if(qx[i] <= 0 || qx[i] >= W - 1) qvx[i] = -qvx[i];
if(qy[i] <= 0 || qy[i] >= H - 1) qvy[i] = -qvy[i];
}

// Draw fading geometric connecting lines
for(int t = 0; t < TAIL; t++) {
if (hx[0][t] == 0 && hy[0][t] == 0) continue; // Skip first frame
uint8_t fade = 255 - (t * (255 / TAIL));
uint16_t c1 = rgb(fade, 0, fade / 2); // Neon Pink
uint16_t c2 = rgb(0, fade, fade); // Neon Cyan
matrix.drawLine(hx[0][t], hy[0][t], hx[1][t], hy[1][t], c1);
matrix.drawLine(hx[1][t], hy[1][t], hx[2][t], hy[2][t], c2);
matrix.drawLine(hx[2][t], hy[2][t], hx[3][t], hy[3][t], c1);
matrix.drawLine(hx[3][t], hy[3][t], hx[0][t], hy[0][t], c2);
}
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 6: Pikachu "Pika Pika!"
// ══════════════════════════════════════════════════════════════════
void drawPikachu() {
matrix.fillScreen(rgb(50, 150, 255)); // Bright Pokémon-sky blue background
// "Pika Pika" speech cadence: open mouth twice quickly, then pause
int talkCycle = frameCount % 60;
bool mouthOpen = (talkCycle > 0 && talkCycle < 10) || (talkCycle > 15 && talkCycle < 25);
// Head bobs down slightly when talking to give it energy
int hover = (mouthOpen) ? 2 : 0;
int cx = 32;
int cy = 34 + hover;
uint16_t yellow = rgb(255, 235, 20);
uint16_t black = rgb(0, 0, 0);
uint16_t red = rgb(255, 30, 30);
uint16_t white = rgb(255, 255, 255);
uint16_t tongue = rgb(255, 100, 100);

// ── 1. Outlines & Black Ear Tips ──
// We draw slightly larger black shapes in the background to act as thick 8-bit outlines!
matrix.fillTriangle(cx - 10, cy - 8, cx - 2, cy - 18, cx - 28, cy - 32, black); // Left Ear
matrix.fillTriangle(cx + 10, cy - 8, cx + 2, cy - 18, cx + 28, cy - 32, black); // Right Ear
matrix.fillRoundRect(cx - 23, cy - 15, 46, 32, 15, black); // Head Outline

// ── 2. Yellow Body ──
// The yellow triangles stop shorter than the black ones to naturally create the black ear tips!
matrix.fillTriangle(cx - 10, cy - 8, cx - 4, cy - 16, cx - 18, cy - 20, yellow); // Left Ear
matrix.fillTriangle(cx + 10, cy - 8, cx + 4, cy - 16, cx + 18, cy - 20, yellow); // Right Ear
matrix.fillRoundRect(cx - 21, cy - 14, 42, 30, 14, yellow); // Head

// ── 3. Face Details ──
// Cheeks
matrix.fillCircle(cx - 15, cy + 5, 5, red);
matrix.fillCircle(cx + 15, cy + 5, 5, red);
// Eyes (Black base, white glint)
matrix.fillCircle(cx - 9, cy - 2, 4, black);
matrix.fillCircle(cx + 9, cy - 2, 4, black);
matrix.fillCircle(cx - 10, cy - 3, 1, white);
matrix.fillCircle(cx + 8, cy - 3, 1, white);

// Nose
matrix.fillRect(cx - 1, cy + 2, 3, 2, black);

// ── 4. Animated Mouth ──
if (mouthOpen) {
// Open Mouth "Pika!"
matrix.fillCircle(cx, cy + 7, 4, black);
matrix.fillCircle(cx, cy + 8, 2, tongue); // Red tongue
// Hide top half of the circle with yellow to make it a 'D' shape
matrix.fillRect(cx - 5, cy + 3, 10, 4, yellow);
} else {
// Closed Mouth 'w' shape
// Left curve
matrix.drawLine(cx - 4, cy + 5, cx - 2, cy + 7, black);
matrix.drawLine(cx - 2, cy + 7, cx, cy + 5, black);
// Right curve
matrix.drawLine(cx, cy + 5, cx + 2, cy + 7, black);
matrix.drawLine(cx + 2, cy + 7, cx + 4, cy + 5, black);
}
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 7: Exact 8-Bit Mario Block Jump
// ══════════════════════════════════════════════════════════════════

// Perfect 16x16 Pixel Map of the "?" Block
const char* blockSprite[16] = {
"KKKKKKKKKKKKKKKK",
"KYYYYYYYYYYYYYDK",
"KYYYYYYYYYYYYYDK",
"KYYYKKKKKKYYYYDK",
"KYYYKWWWWKYYYYDK",
"KYYKWWKKWWKYYYDK",
"KYYKWKYYKWWKKYDK",
"KYYYKKYYKWWKKYDK",
"KYYYYYYKWWKKYDKK",
"KYYYYYYKWWKKYDKK",
"KYYYYYYYKKYYYYDK",
"KYYYYYKWWKYYYYDK",
"KYYYYYKWWKYYYYDK",
"KYYYYYYKKYYYYYDK",
"KDDDDDDDDDDDDDDK",
"KKKKKKKKKKKKKKKK"
};

// Perfect 16x16 Pixel Map of Mario (Modern 8-Bit Style)
const char* marioSprite[16] = {
".....KKKKK......",
"....KRRRRRK.....",
"...KRRRRRRRRK...",
"...KBBBSKSK.K...",
"..KBSBSSSBSSSK..",
"..KBSBSSSSSSSK..",
"..KBBSSSSSSBBK..",
"...KSSSSSSS.K...",
"..KKKRLRRRKKK...",
".KSSKLLRLLRKSSK.",
".KSSKLLRLLRKSSK.",
".KSSKLLYLLRKSSK.",
"..KKKLLLLLLKKK..",
"...KKLLLLLLKK...",
"..KBBBK..KBBBK..",
".KBBBBK..KBBBBK."
};

// Extremely fast sprite renderer reading the character map
void drawSprite(int ox, int oy, const char* sprite[], int scale) {
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
char c = sprite[y][x];
if (c == '.') continue; // Skip transparency
uint16_t color = 0;
if (c == 'K') color = rgb(0,0,0); // Black Outline
else if (c == 'Y') color = rgb(255,255,0); // Yellow
else if (c == 'D') color = rgb(200,120,0); // Dark Orange Shading
else if (c == 'W') color = rgb(255,255,255); // White
else if (c == 'R') color = rgb(255,0,0); // Mario Red
else if (c == 'B') color = rgb(120,60,0); // Mario Brown
else if (c == 'S') color = rgb(255,200,140); // Mario Skin
else if (c == 'L') color = rgb(0,80,255); // Mario Blue
matrix.fillRect(ox + (x * scale), oy + (y * scale), scale, scale, color);
}
}
}

void drawMarioBlock() {
matrix.fillScreen(rgb(100, 180, 255)); // Super Mario Sky Blue

// Ground Base
matrix.fillRect(0, 56, 64, 8, rgb(200, 76, 12)); // Brick Red Ground
matrix.drawFastHLine(0, 56, 64, rgb(0,0,0)); // Ground Outline

int cycle = frameCount % 80;
int mY = 24; // Base Mario Y (Resting on the ground)
int bY = 8; // Base Block Y

// Mario Jump Physics (Sine Wave)
if (cycle >= 10 && cycle <= 40) {
float t = (cycle - 10) / 30.0f; // Arc from 0.0 to 1.0
mY = 24 - (int)(sinf(t * 3.14159f) * 16.0f); // 16px high jump
}

// Block Bump Collision
if (cycle >= 23 && cycle <= 27) {
bY = 4; // Block jumps up when hit by Mario's head!
}

// Coin Pop-up!
if (cycle >= 24 && cycle <= 36) {
float ct = (cycle - 24) / 12.0f;
int cY = 8 - (int)(sinf(ct * 3.14159f) * 16.0f);
matrix.fillRect(28, cY, 8, 12, rgb(255, 255, 0)); // Coin Body
matrix.drawRect(28, cY, 8, 12, rgb(200, 120, 0)); // Coin Outline
}

// Draw the entities (Scale = 2 makes them nice and chunky!)
drawSprite(16, bY, blockSprite, 2);
drawSprite(16, mY, marioSprite, 2);
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 8: The Matrix Digital Rain
// ══════════════════════════════════════════════════════════════════
int rainY[W];
void drawMatrixRain() {
// Setup arrays on the very first frame of the animation
if (frameCount == 0) {
for(int i=0; i<W; i++) rainY[i] = random(-60, 0);
}
matrix.fillScreen(0);
for (int x = 0; x < W; x++) {
rainY[x] += random(1, 3); // Fall speed
if (rainY[x] > H + 15) rainY[x] = random(-20, 0);
// Draw trail
for (int t = 0; t < 15; t++) {
int y = rainY[x] - t;
if (y >= 0 && y < H) {
if (t == 0) matrix.drawPixel(x, y, rgb(200, 255, 200)); // Bright head
else matrix.drawPixel(x, y, rgb(0, 255 - (t * 15), 0)); // Fading tail
}
}
}
}

// ══════════════════════════════════════════════════════════════════
// MODE: Audio Reactive Waveform (Standalone)
// ══════════════════════════════════════════════════════════════════
void drawWaveform() {
static float samples[OVERSAMP];
uint32_t sum = 0;

for (int i = 0; i < OVERSAMP; i++) {
int v = analogRead(MIC_PIN);
samples[i] = (float)v;
sum += v;
delayMicroseconds(100); // Slight speed up for better sampling
}

// DEBUG: Uncomment this line to check values in Serial Monitor (115200)
// Serial.println(sum / OVERSAMP);

float dc = (float)sum / (float)OVERSAMP;
float rmsSum = 0.0f;
for (int i = 0; i < OVERSAMP; i++) {
float centered = samples[i] - dc;
rmsSum += centered * centered;
}
float rms = sqrtf(rmsSum / (float)OVERSAMP);

if (!noiseInit) { noiseFloor = rms; noiseInit = true; }
noiseFloor = (0.95f * noiseFloor) + (0.05f * rms);

float rmsEffective = rms - (noiseFloor * 1.2f); // Slightly higher gate
if (rmsEffective < 0.0f) rmsEffective = 0.0f;

rmsPeak = (0.99f * rmsPeak) + (0.01f * rmsEffective);
float loud = (rmsPeak > 5.0f) ? (rmsEffective / rmsPeak) : 0.0f;
loudSmooth = (0.7f * loudSmooth) + (0.3f * loud);

// Build points
for (int i = 0; i < NUM_POINTS; i++) {
float s = samples[i * (OVERSAMP / NUM_POINTS)];
float amp = loudSmooth * (H / 2.0f);
float y = (H / 2.0f) - ((s - dc) * amp / 500.0f);
if (y < 0) y = 0; if (y >= H) y = H - 1;
waveY[i] = y;
}

uint16_t waveColor = rgb(0, 255, 0); // Solid green wave
for (int i = 1; i < NUM_POINTS; i++) {
matrix.drawLine((i - 1) * X_STEP, (int)waveY[i - 1], i * X_STEP, (int)waveY[i], waveColor);
}
}

void setup() {
Serial.begin(115200);
analogReadResolution(12);
pinMode(BTN_PREV, INPUT_PULLUP);
pinMode(BTN_NEXT, INPUT_PULLUP);
pinMode(BTN_WAVE, INPUT_PULLUP);
matrix.begin();
}

void handleButtons() {
unsigned long now = millis();
bool rN = digitalRead(BTN_NEXT), rP = digitalRead(BTN_PREV), rW = digitalRead(BTN_WAVE);
if (now - lastDebounceTime > debounceDelay) {
if (rN != lastBtnNext && rN == LOW) { currentAnim = (currentAnim + 1) % NUM_ANIMS; frameCount = 0; lastDebounceTime = now; }
if (rP != lastBtnPrev && rP == LOW) { currentAnim = (currentAnim - 1 + NUM_ANIMS) % NUM_ANIMS; frameCount = 0; lastDebounceTime = now; }
if (rW != lastBtnWave && rW == LOW) { waveMode = !waveMode; lastDebounceTime = now; }
}
lastBtnNext = rN; lastBtnPrev = rP; lastBtnWave = rW;
}

void loop() {
handleButtons();
if (millis() - previousMillis >= frameInterval) {
previousMillis = millis();
matrix.fillScreen(0);
if (waveMode) {
drawWaveform();
} else {
switch (currentAnim) {
case 0: drawPacGhost(); break; case 1: drawCyberpunkRelic(); break; case 2: drawRetroFire(); break;
case 3: drawXORFractal(); break; case 4: drawSynthwave(); break; case 5: drawNeonQix(); break;
case 6: drawPikachu(); break; case 7: drawMarioBlock(); break; case 8: drawMatrixRain(); break;
}
}
matrix.show(); frameCount++;
}
}

코드는 다음과 같이 구성됩니다.





STEP 21. 매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


매트릭스 조립


  1. 이제 최종 조립 과정을 진행합니다. 먼저 피코 드라이버 보드를 RGB 매트릭스에 다시 연결합니다. 매트릭스의 VCC 및 GND 선을 피코 드라이버 보드의 5V 및 GND 단자에 납땜합니다.
  2. 다음으로, 첫 번째 매트릭스의 16핀 리본 케이블을 피코 드라이버 보드의 HUB75 커넥터에 다시 연결합니다.
  3. 더 진행하기 전에, 설정을 켜서 모든 것이 정상적으로 작동하는지 확인합니다. 매트릭스와 디스플레이가 모두 제대로 작동하는 것을 확인한 후, 인클로저 조립을 시작합니다.
  4. 본체는 장착 구멍을 RGB 매트릭스의 장착 지점과 맞춘 후 디스플레이 위에 올려놓습니다. 그런 다음 총 10개의 M3 볼트를 사용하여 두 매트릭스를 본체에 고정합니다.
  5. 다음으로, 피코 드라이버 보드의 장착 구멍을 매트릭스의 장착 지점과 맞춘 후 두 매트릭스 사이에 위치시킵니다. 그런 다음 M3 볼트 2개를 사용하여 피코 드라이버 보드를 제자리에 고정합니다.
  6. 세 개의 주황색 스위치 액추에이터를 각각의 슬롯에 배치한 뒤, 주황색 그릴 부품을 장착합니다. 그런 다음 스위치 보드를 세 개의 액추에이터 위에 올려놓고 M2 나사를 사용하여 고정합니다.
  7. 다음으로, 주 전원 스위치용 주황색 액추에이터를 해당 슬롯에 배치합니다.
  8. 그런 다음 3.7V 리튬이온 셀을 인클로저 내부에 넣습니다. 파워 보드를 메인 스위치 액추에이터와 리튬이온 셀 위에 위치시킨 후 M2 나사를 사용하여 고정합니다.
  9. 마지막으로, 후면 덮개를 본체 뒷면에 위치시킵니다. 좌우 양쪽에서 M2 나사를 삽입하여 조여 후면 덮개를 본체에 고정하면 조립 과정이 완료됩니다.




STEP 22. 결과


결과


결과


결과


결과


그리고 이 프로젝트의 최종 결과물인 ‘PROJECT GESTALT’를 소개합니다. 이 프로젝트는 애니메이션, 게임에서 영감을 받은 비주얼, 소리에 반응하는 WaveForm 모드를 모두 결합한 완전한 기능을 갖춘 RGB 매트릭스 디스플레이로, ‘NieR: Automata’의 ‘Pods’에서 영감을 받은 맞춤형 케이스 안에 담겨 있습니다.

기기를 켜려면 메인 전원 버튼을 누르면 되며, 그러면 시스템이 전원이 켜지고 디스플레이가 초기화됩니다.

세 개의 제어 버튼, 즉 ‘이전(Previous)’, ‘웨이브폼 모드(WaveForm Mode)’, ‘다음(Next)’이 제공됩니다.

'이전' 및 '다음' 버튼을 사용하면 기기에 저장된 다양한 애니메이션을 순차적으로 볼 수 있습니다. '다음' 버튼을 누르면 다음 애니메이션으로 이동하고, '이전' 버튼을 누르면 이전 애니메이션으로 돌아갑니다.

전용 웨이브폼 모드 버튼을 누르면 기기가 소리에 반응하는 모드로 전환됩니다. 이 모드에서는 마이크가 주변 소리의 크기를 감지하여, 이를 RGB 매트릭스 상의 애니메이션 파형으로 실시간으로 시각화합니다. 주변 소리가 커지면 파형이 확장되며 이에 따라 반응합니다.




STEP 23. 애니메이션


애니메이션


애니메이션


애니메이션


애니메이션


애니메이션


애니메이션


애니메이션


애니메이션


Project Gestalt에는 현재 9개의 맞춤형 애니메이션이 포함되어 있으며, 각 애니메이션은 라즈베리 파이 피코에서 원활하게 실행될 수 있도록 절차적 그래픽과 경량 코드를 사용하여 제작되었습니다.

  1. 사이버 고스트(Cyber-Ghost) - 발이 움직이고 RGB 색상이 끊임없이 변화하는, 팩맨에서 영감을 받은 다채로운 유령입니다.
  2. Cyberpunk 2077 Relic - 글리치 효과, 화면 찢김 현상, 무작위 디지털 간섭이 특징인 사이버펑크 스타일의 Relic 로고입니다.
  3. 레트로 DOOM 불꽃 - 올드스쿨 게임과 데모씬에서 영감을 받은 고전적인 픽셀 화염 효과로, 화면을 가로질러 불길이 역동적으로 치솟습니다.
  4. XOR 사이버 프랙탈 - 경량 비트 연산과 절차적 애니메이션을 사용하여 생성된, 끊임없이 진화하는 기하학적 패턴입니다.
  5. Synthwave Outrun Grid - 80년대 복고풍에서 영감을 받은 애니메이션 일몰과 움직이는 원근법 그리드가 앞으로 나아가는 듯한 착시를 만들어냅니다.
  6. Neon Qix Trails - 고전적인 Mystify 스크린세이버에서 영감을 받아, 튀어 오르는 네온 선들이 뒤로 다채로운 색상의 희미해지는 흔적을 남기는 화면입니다.
  7. Pikachu - 고전적인 “피카 피카!” 표정을 재현하기 위해 고개를 끄덕이고 입을 움직이는 애니메이션 8비트 피카츄입니다. (이것이 제가 가장 좋아했던 작품입니다)
  8. It's a Me Mario - 마리오가 달리고, 물음표 블록 속으로 뛰어들어 금화를 튕겨내는 미니어처 애니메이션 장면입니다.
  9. 매트릭스 디지털 레인 - 디스플레이 전체에 빛나는 녹색 코드 줄기가 끊임없이 쏟아지는, 영화 ‘매트릭스’에서 영감을 받은 상징적인 효과입니다.

본문 계속 3/3




STEP 24. 파형 모드


파형 모드


제가 ‘프로젝트 게슈탈트’에 도입한 또 다른 기능은 제 이전 프로젝트 중 하나인 ‘웨이브폼: 배경음 시각화 도구’에서 가져온 것입니다.

WaveForm은 주변 소리를 HUB75 RGB 매트릭스에 표시되는 애니메이션 파형으로 변환하는 소리 반응형 비주얼라이저입니다. 소리가 커지면 파형이 확장되며 실시간으로 반응합니다.

프로젝트 게슈탈트(Project Gestalt)는 이미 대형 RGB 매트릭스를 사용하고 있기 때문에, 이전에 개발했던 웨이브폼 코드를 이 프로젝트에도 이식했습니다. 전용 웨이브폼 버튼을 누르면 기기가 오디오 반응 모드로 전환되어, 표시된 파형이 주변 소리에 실시간으로 반응합니다.

아래 링크에서 WaveForm에 대한 전체 기사를 확인해 보실 수 있습니다.

https://www.instructables.com/WaveForm-the-Background-Sound-Visualizer/

다음은 듀얼 RGB 패널 설정에 맞춰 수정했던 코드입니다.

void drawWaveform() {
static float samples[OVERSAMP];
uint32_t sum = 0;

for (int i = 0; i < OVERSAMP; i++) {
int v = analogRead(MIC_PIN);
samples[i] = (float)v;
sum += v;
delayMicroseconds(100); // 더 나은 샘플링을 위해 속도를 약간 높임
}

// 디버그: 시리얼 모니터(115200)에서 값을 확인하려면 이 줄의 주석 처리를 해제하세요
// Serial.println(sum / OVERSAMP);

float dc = (float)sum / (float)OVERSAMP;
float rmsSum = 0.0f;
for (int i = 0; i < OVERSAMP; i++) {
float centered = samples[i] - dc;
rmsSum += centered * centered;
}
float rms = sqrtf(rmsSum / (float)OVERSAMP);

if (!noiseInit) { noiseFloor = rms; noiseInit = true; }
noiseFloor = (0.95f * noiseFloor) + (0.05f * rms);

float rmsEffective = rms - (noiseFloor * 1.2f); // 게이트를 약간 높임
if (rmsEffective < 0.0f) rmsEffective = 0.0f;

rmsPeak = (0.99f * rmsPeak) + (0.01f * rmsEffective);
float loud = (rmsPeak > 5.0f) ? (rmsEffective / rmsPeak) : 0.0f;
loudSmooth = (0.7f * loudSmooth) + (0.3f * loud);

// 데이터 포인트 생성
for (int i = 0; i < NUM_POINTS; i++) {
float s = samples[i * (OVERSAMP / NUM_POINTS)];
float amp = loudSmooth * (H / 2.0f);
float y = (H / 2.0f) - ((s - dc) * amp / 500.0f);
if (y < 0) y = 0; if (y >= H) y = H - 1;
waveY[i] = y;
}

uint16_t waveColor = rgb(0, 255, 0); // 단색 녹색 파도
for (int i = 1; i < NUM_POINTS; i++) {
matrix.drawLine((i - 1) * X_STEP, (int)waveY[i - 1], i * X_STEP, (int)waveY[i], waveColor);
}
}




STEP 25. 결론


결론


결론


결론


결론


이 프로젝트는 성공적이었으며, ‘프로젝트 게슈탈트’에서 제가 가장 마음에 드는 점 중 하나는 바로 그 다용도성입니다. 벽에 걸 수도 있고 스탠드를 이용해 책상 위에 놓을 수도 있어, 제 작업 공간에 상호작용적이면서도 미적으로 훌륭한 요소가 되어줍니다.

이 기기에는 현재 9가지의 맞춤형 애니메이션과 소리에 반응하는 ‘WaveForm’ 모드가 탑재되어 있습니다. 향후 더 많은 애니메이션을 쉽게 추가할 수 있어, 시간이 지나도 디스플레이가 항상 신선하고 흥미롭게 유지될 것입니다.

또한 이 프로젝트의 다음 버전을 위해 몇 가지 업그레이드 계획을 세우고 있습니다. 주요 개선 사항 중 하나는 라즈베리 파이 피코(Raspberry Pi Pico)를 피코 W(Pico W)로 교체하여 Wi-Fi 기능을 추가하는 것입니다. 이를 통해 이미지나 기타 콘텐츠를 매트릭스로 무선으로 전송할 수 있게 될 것입니다. 또한 DFPlayer Mini 모듈을 추가하여 프로젝트에 오디오 재생 기능을 도입하고 싶습니다.

이러한 업그레이드를 통해 프로젝트 게슈탈트의 다음 버전이 더욱 상호작용적이고 기능이 풍부해지기를 기대합니다. 현재로서는 이번 버전의 완성도에 꽤 만족하고 있으며, 여러분도 이 프로젝트를 마음에 들어 주셨기를 바랍니다.

현재 ‘프로젝트 게슈탈트’는 완성되었으며, 코드, 회로도, 케이스 파일, 그리고 전체 제작 지침을 포함하여 여러분이 직접 제작하는 데 필요한 모든 세부 정보는 이 글에서 확인하실 수 있습니다.

여기까지 읽어주신 분들께 특별히 감사드리며, 제가 이 프로젝트를 제작하는 즐거움만큼이나 여러분도 이 글을 읽는 즐거움을 느끼셨기를 바랍니다.

조만간 또 다른 프로젝트로 돌아오겠습니다. 그때까지 계속 지켜봐 주시고, 즐거운 제작 시간 보내세요!

회사명 공방유니언 주소 각 공방 주소 참조
사업자 등록번호 0000 대표 공방유니언 전화 019-60105846 팩스 0000 이메일 gongbangunion@gmail.com
통신판매업신고번호 0000 개인정보관리책임자 공방유니언
Copyright © 2017 공방유니언. All Rights Reserved.