
Vicsek 모델로 보는 물고기 군집 움직임
Vicsek 모델을 p5.js 물고기 떼 애니메이션으로 옮기며 지역 정렬과 Boids의 분리 규칙을 함께 쓴 이유를 설명합니다.
Photo by Ian Livesey on StockSnap
물고기 여러 마리가 한 화면에서 움직이면 처음에는 꽤 그럴듯해 보입니다.
각 물고기마다 속도와 회전값을 조금씩 다르게 주고 꼬리를 흔들고 화면 가장자리에서 방향을 바꾸게 만들면 “살아 있는 것 같은” 애니메이션은 금방 나옵니다. 저도 처음에는 그 정도면 충분하다고 생각했습니다.
그런데 조금 더 보고 있으면 아쉬운 점이 보입니다. 물고기들이 같은 장면 안에 있을 뿐, 서로를 보고 움직이는 것 같지는 않습니다. 한 마리가 방향을 틀어도 주변 물고기에게 영향이 없고 무리가 찢어졌다가 다시 맞춰지는 순간도 생기지 않습니다.
집단 움직임을 보고 싶다면 질문이 달라져야 합니다.
각 개체는 단순하게 움직이는데, 왜 전체는 한 덩어리처럼 보일까?
이 글에서는 그 질문을 Vicsek 모델에서 출발해 설명합니다. 그리고 순수한 수학 모델을 그대로 구현하지 않고 화면에 그려지는 물고기에 맞게 Boids separation을 섞은 이유까지 함께 정리합니다.
임의의 움직임과 집단 움직임은 다르다
임의의 물고기 애니메이션은 보통 개체 하나를 예쁘게 움직이는 문제로 시작합니다. 위치를 갱신하고 방향을 조금 흔들고 몸통을 그 방향에 맞춰 그리면 됩니다.
하지만 집단 운동(collective motion)은 개체 하나보다 관계가 중요합니다. 각 물고기가 주변 물고기의 방향을 보고 자기 방향을 조금 조정해야 합니다. 이때 보는 건 가까운 이웃뿐입니다. 멀리 있는 무리 전체를 아는 건 아닙니다.
이 차이가 작아 보이지만 결과는 크게 달라지죠.
혼자 흔들리는 물고기 50마리는 예쁜 배경이 될 수 있습니다. 하지만 서로의 방향을 조금씩 따라가는 물고기 50마리는 어느 순간 작은 무리를 만들고 무리끼리 부딪히고 때로는 화면 전체가 한 방향으로 흐르는 것처럼 보입니다.
Vicsek 모델은 바로 이 차이를 아주 단순한 규칙으로 압축합니다.
Vicsek 모델의 최소 규칙
Vicsek 모델은 자기 추진 입자(self-driven particles), 즉 스스로 움직이는 입자들을 다룹니다. 각 입자는 점으로 표현되고 매 시간 같은 크기의 속도로 앞으로 나아갑니다. 그러니 실제로 달라지는 값은 방향뿐입니다.
각 입자는 다음 순서로 갱신됩니다.
- 자기 주변 반경 안에 있는 이웃을 찾는다.
- 그 이웃들의 진행 방향 평균을 구한다.
- 평균 방향에 약간의 noise를 더한다.
- 같은 속도로 그 방향을 향해 움직인다.
식으로 쓰면 대략 이렇게 볼 수 있습니다.
θ_i(t + 1) = 평균방향(θ_j(t), j는 i 주변 반경 r 안의 이웃) + noise
x_i(t + 1) = x_i(t) + v · 방향벡터(θ_i(t + 1))여기서 θ_i는 i번째 입자의 진행 방향이고 r은 상호작용 반경(interaction radius)입니다. v는 한 스텝에 움직이는 거리입니다. 실제 구현에서는 각도를 그냥 산술 평균하면 문제가 생깁니다. 그래서 sin과 cos를 평균낸 뒤 다시 각도로 돌리는 순환 평균(circular mean)을 씁니다.
예를 들어 359도와 1도는 거의 같은 방향입니다. 그런데 숫자로만 평균을 내면 180도가 됩니다. 물고기 둘이 오른쪽을 보고 있는데 평균은 왼쪽이 되는 이상한 결과가 나옵니다. circular mean은 이런 각도 경계 문제를 피하려고 방향을 단위 벡터처럼 다룹니다.
noise는 정렬이 얼마나 불완전한지를 표현합니다. noise가 작으면 주변 방향을 꽤 잘 따라가고 noise가 커지면 방향이 더 많이 흔들립니다. Vicsek 논문에서는 이 noise가 온도처럼 작동한다고 설명합니다. 너무 흔들리면 질서가 깨지고 충분히 작으면 정렬된 운동이 나타납니다.
반경을 키우면 무엇이 달라질까
상호작용 반경은 “누구를 이웃으로 볼 것인가”를 정합니다.
반경이 작으면 각 물고기는 아주 가까운 물고기 몇 마리만 봅니다. 그래서 작은 그룹이 생기기 쉽고 그룹마다 방향이 다를 수 있습니다. 화면을 보면 군데군데 미니 무리가 생겼다가 갈라지는 느낌이 납니다.
반경이 커지면 한 물고기가 참고하는 이웃 수가 늘어납니다. 방향 정보가 더 멀리 퍼지고 작은 그룹들이 서로 영향을 주기 시작합니다. noise가 충분히 낮으면 화면 전체가 한 방향으로 정렬되는 순간도 볼 수 있습니다.
이 지점이 Vicsek 모델이 재미있는 이유입니다. 개체는 여전히 단순한 규칙만 따르는데 반경과 noise의 조합에 따라 무질서한 상태와 정렬된 상태가 갈라집니다. 엄밀한 물리 실험을 하는 글은 아니지만 이 데모에서도 작은 지역 규칙이 전체 흐름으로 번지는 감각은 관찰할 수 있죠.
화면 위의 물고기는 점이 아니다
순수한 Vicsek 모델에서 입자는 점입니다. 점끼리는 겹쳐도 시각적으로 크게 문제가 되지 않습니다. 하지만 우리가 그리는 대상은 물고기 몸체입니다. 머리, 몸통, 지느러미, 꼬리가 있고 실제 픽셀 영역을 차지합니다.
Vicsek식 정렬만 넣으면 물고기들이 같은 방향으로 잘 움직이긴 합니다. 대신 가까운 곳에 몰렸을 때 몸이 겹쳐 보일 수 있습니다. 수학 모델에서는 문제가 아니던 장면이 애니메이션에서는 읽기 어려운 장면이 되는 거죠.
그래서 이 구현은 순수 Vicsek 모델이 아니라 교육용 하이브리드 데모에 가깝습니다.
방향을 맞추는 주된 규칙은 Vicsek 모델에서 가져왔습니다. 하지만 너무 가까운 물고기는 서로 살짝 밀어내도록 Boids의 separation 아이디어를 더했습니다. 여기서 separation은 집단 움직임의 주인공이 아닙니다. 화면에서 물고기 몸체가 서로 뭉개지지 않게 하는 시각화 보정에 가깝습니다.
직접 반경, noise, separation을 바꿔보기
아래 데모에서 세 슬라이더를 움직여보세요. 한 번에 하나씩 바꾸면 차이가 더 잘 보입니다.
Interaction radius는 각 물고기가 참고하는 이웃의 범위를 바꿉니다. 낮추면 가까운 물고기끼리 작은 그룹을 만들고 높이면 더 큰 무리가 같은 방향으로 맞춰지기 쉽습니다.
Noise는 평균 방향에 더하는 무작위 흔들림입니다. 낮추면 정렬이 빨라지고 높이면 물고기들이 계속 방향을 잃습니다. 너무 높이면 이웃을 보더라도 전체 흐름이 안정되기 어렵습니다.
Separation은 몸이 너무 가까워졌을 때 밀어내는 거리입니다. 낮추면 물고기들이 더 촘촘히 모이고 높이면 서로 간격을 크게 유지합니다. 이 값을 너무 크게 올리면 정렬보다 간격 유지가 더 눈에 띌 수 있죠.
구현은 역할을 나눠서 읽으면 쉽다
이 데모는 p5.js 스케치 하나에 모든 로직을 넣지 않았습니다. 모델, 몸체, 렌더링, React 셸을 나눠두면 “무엇이 집단 행동이고, 무엇이 화면 표현인가”를 구분하기 쉽습니다.
simulation.ts는 agent 상태와 갱신 규칙을 다룹니다. 이웃을 찾고 circular mean으로 정렬 방향을 구하고 noise를 더합니다. 그 뒤 separation과 boundary steering을 섞고 제한된 회전 속도로 다음 heading을 정합니다.
fish-rig.ts는 점으로 된 agent를 물고기 몸체로 바꿉니다. 머리 위치와 heading을 기준으로 여러 joint를 이어 붙이고 각 joint의 각도 변화를 제한해 꼬리가 갑자기 꺾이지 않게 합니다.
renderer.ts는 rig를 p5.js 도형으로 그립니다. 몸통 폭, 지느러미, 꼬리, 눈을 그리지만 flocking 규칙은 모릅니다. 그 덕분에 렌더링을 바꿔도 시뮬레이션 규칙을 건드리지 않아도 됩니다.
hybrid-fish-demo.tsx는 React와 p5.js 사이의 경계입니다. 캔버스 크기를 잡고 p5 인스턴스 생명주기를 관리하고 슬라이더 값을 simulation.ts의 config로 넘깁니다.
이렇게 나누면 글을 읽는 사람도 코드를 따라가기 쉬워집니다. “반경을 바꾸면 어디가 바뀌지?”라는 질문은 hybrid-fish-demo.tsx에서 시작해 simulation.ts로 내려가면 됩니다. “물고기 모양이 왜 이렇게 휘지?”라는 질문은 fish-rig.ts와 renderer.ts를 보면 됩니다.
모델을 그대로 옮기는 것과 화면에 번역하는 것
Vicsek 모델의 아름다움은 단순함에 있습니다. 가까운 이웃의 방향을 평균내고 noise를 더하고 같은 속도로 움직인다. 이 정도 규칙만으로도 정렬된 집단 움직임이 나타납니다.
하지만 블로그 데모는 논문 속 입자 시뮬레이션과 목적이 다릅니다. 독자가 브라우저에서 보고 슬라이더를 만지고 “아 이 값이 이런 변화를 만들었구나”라고 느껴야 합니다. 그러려면 점 대신 몸을 가진 물고기를 그려야 하고 겹침과 화면 경계도 신경 써야 합니다.
그래서 이 구현은 모델의 충실도와 화면에서 읽히는 표현 사이에서 타협합니다. Vicsek 모델의 지역 정렬은 중심에 두고 화면에서 읽히지 않는 부분은 Boids의 분리 규칙과 렌더링 구조로 보정했습니다.
완전히 순수한 모델은 아니지만 교육용 데모로는 오히려 이 경계가 중요하다고 생각합니다. 수식이 화면으로 옮겨질 때 어떤 부분이 유지되고 어떤 부분이 시각화를 위해 바뀌는지 볼 수 있으니까요.
참고 자료
- Vicsek et al., “Novel Type of Phase Transition in a System of Self-Driven Particles”, Physical Review Letters, 1995: https://harvest.aps.org/v2/journals/articles/10.1103/PhysRevLett.75.1226/fulltext
- Craig Reynolds, “Flocks, Herds, and Schools: A Distributed Behavioral Model”, SIGGRAPH 1987: https://www.red3d.com/cwr/papers/1987/boids.html