AI에게 논문과 OSS를 조사하게 하여 Three.js의 물고기를 '생명체'로 만들다
요약
본 글은 웹 낚시 게임 '기술 낚시'의 물고기 구현 과정을 다루며, 단순히 모델링을 넘어 생명체 같은 움직임을 만드는 데 초점을 맞추었습니다. 특히 AI를 리서처 및 리뷰어 역할로 활용하여 실제 어류학 논문과 관련 연구 결과를 조사하고, 이를 Three.js 기반 게임에 적용하는 개발 플로우를 소개합니다.
핵심 포인트
- AI를 이용해 전문 분야(어류학)의 지식을 탐색/요약하는 방법을 제시함.
- 물고기의 움직임은 단순한 반복 패턴이 아닌 복합적인 생명체적 특성을 가져야 함을 강조함.
- 연구 결과를 그대로 재현하기보다, 게임 표현에 필요한 아이디어를 참고하고 조정하는 것이 중요함.
- AI를 완성품 제작보다는 리서치 및 리뷰 과정에서 활용하는 방법을 보여줌.
안녕하세요, 와카토입니다.
최근 스마트폰을 낚싯대로 활용해 놀 수 있는 웹 낚시 게임 '기술 낚시(技術釣り)' 를 제작하고 있습니다.
PC 화면에 바다를 표시하고, QR 코드를 스마트폰으로 읽으면 그 스마트폰이 낚싯대가 됩니다.
스마트폰을 흔들어 캐스팅하고, 물고기가 걸리면 낚아 올립니다.
게다가 일반적인 물고기뿐만 아니라 Go나 Docker 같은 기술의 특징을 물고기의 외형이나 움직임에 녹여내어,
놀다 보면 자연스럽게 기술적 특징까지 알 수 있다
같은 게임을 만들고 있습니다.
발표 자료를 만들면서 공들인 포인트를
- 물고기의 움직임
- 물고기의 외형
- 바다의 그래픽
정도에 집중하려고 했습니다.
그런데 Git 히스토리와 구현을 되돌아보던 중,
'이거, 그냥 3줄로 끝낼 수 있는 양이 아닌데?'
라는 생각이 들었습니다.
특히 물고기 부분은,
- 헤엄치는 방식
- 도망치는 방식
- 지느러미의 움직임
- 수중에서의 보이게 하는 방식
- 수면을 넘는 굴절
- 발광하는 방식
- 줄과의 연결성
- 낚싯대에 힘이 전달되는 방식
까지 상당히 많은 부분을 다루었습니다.
이번 글에서는 그중에서도
'물고기가 움직이는 것'에서 '물고기를 낚는 것'으로 변화하는 과정
에 어떤 작업을 했는지 적어보겠습니다.
우선 여기서 한 가지 말씀드립니다.
제가 어류학이나 수중 광학 논문을 대량으로 읽고, 모든 것을 이해한 후에 수식을 재현했다는 이야기는 아닙니다.
연구의 탐색・요약・비교에는 AI를 상당히 많이 사용했습니다.
이번 개발 플로우는 대체로 다음과 같았습니다.
실제로 게임을 구동하기
↓
'뭔가 물고기 같지 않다'
...
AI에게,
'물고기는 실제로 어떻게 휘어지는 걸까?'
'꼬리 지느러미는 계속 똑같은 방식으로 움직이는 걸까?'
'수면 너머의 물고기는 어떻게 보일까?'
'Three.js로 비슷한 구현이 없을까?'
같은 것들을 상당히 조사하게 했습니다.
여기서 소개하는 연구들은 연구 결과 그 자체를 충실히 재현했다는 의미가 아닙니다.
AI를 이용해 관련 연구를 조사・비교시키고, 그중에서 게임 표현에 사용할 만한 아이디어를 참고하고 있습니다.
구현 내의 진폭(amplitude)・감쇠율(decay rate)・시인성 보정 등에는 게임용으로 직접 조정한 값도 많이 포함되어 있습니다.
개인적으로 이번에는,
AI에게 완성품을 만들게 하는 것
보다는,
AI를 리서처 겸 리뷰어 역할로 사용하는 것
이 상당히 재미있었습니다.
물고기 모델 만들기.
바다에 넣기.
헤엄치게 하기.
바늘에 걸리게 하기.
릴로 앞쪽까지 끌어당기기.
기능만 보면 제대로 된 낚시입니다.
하지만 실제로 플레이해보면,
뭔가 물고기가 아니다.
더 정확히 말하면,
바다에 떨어진 쓰레기를 줄로 끌어당기는 느낌이 든다.
처음에는 단순히,
'모델의 퀄리티가 낮아서 그런가?'
라고 생각했습니다.
하지만 AI에게 코드와 움직임을 분석하게 하면서, 원인은 모델 하나만은 아니었습니다.
예를 들어,
- 감을 때 물고기가 플레이어 쪽으로 향한다.
- 상태가 바뀔 순간에 갑자기 방향을 전환한다.
- 몸 전체가 같은 주기로 흔들린다.
- 지느러미까지 전부 똑같이 움직인다.
- 수면 너머에서는 몸체가 찌그러진다.
- 어두운 몸체보다 발광선만 눈에 띈다.
- 줄이 물고기 입과 연결되어 보이지 않는다.
등.
하나하나만 보면 작은 이질감입니다.
하지만 이것들이 쌓이면,
3D 모델이지만, 생명체는 아니다.
라는 인식이 되었습니다.
여기서부터 하나씩 개선해 나갔습니다.
먼저 AI에게 조사하게 한 것은 실제 물고기의 유영(遊泳)입니다.
Flammang & Lauder의 블루길 연구에서는 가속・제동・후퇴 등에 따라 꼬리 지느러미의 형태나 근육 활동이 변하는 것이 측정되었습니다.
다른 연구에서도 헤엄치는 속도에 따라 꼬리 지느러미 내부의 근육 활동이 변화합니다.
즉,
물고기 = 영원히 같은 sin파를 흘려보내면 되는 것
은 아닌 것 같습니다.
그래서 기술 낚시에서는 물고기의 헤엄에 여러 상태를 만들었습니다.
const GAIT_PROFILES = {
cruise: {
acceleration: 1.8,
...
대략적으로는,
| 상태 | 역할 |
|---|---|
cruise | 평범하게 헤엄치기 |
turn | 방향 전환 |
burst | 급가속 |
coast | 관성 |
hooked_burst | 바늘에 걸린 직후 |
exhausted | 피로 |
이런 식입니다.
cruise 상태에서는 진폭(amplitude) 0.08 / 주파수 1.6인 반면, 바늘에 걸린 직후의 hooked_burst 상태에서는 0.30 / 5.8까지 높였습니다. 반대로 피로 상태에서는 0.025 / 0.6까지 낮춥니다.
여기서 사용된 구체적인 수치는 논문의 값이 아닙니다.
연구에서 얻은 것은,
운동 상태에 따라 신체의 사용 방식이 달라진다
라는 생각입니다.
수치 자체는 게임 화면을 보면서 조정했습니다.
후킹하는 순간에도 일반적인 cruise 상태만으로는 상당히 생기가 없습니다.
그래서 바늘에 걸린 직후에만 전용의 급가속(acceleration)을 넣었습니다.
triggerCStart(escapeDirection: Vec3): void {
const direction = normalise(escapeDirection);
this.cStartRemaining = 0.22;
...
물고기에는 자극을 받은 순간 몸을 크게 구부리며 도망치는 C-start라는 행동이 있습니다.
기술 낚시에서는 그 생체 운동을 정확하게 재현하고 있는 것은 아닙니다.
다만,
'걸린 순간에 한 번에 도망친다'
라는 연출의 참고로 삼았습니다.
후킹하면 0.22초 동안 hooked_burst 상태로 전환됩니다.
이것만으로도,
'맞는 순간 물고기가 저항했다'
는 느낌이 상당히 커졌습니다.
여기가 이번에 가장 재미있었던 부분입니다.
처음 구현했을 때는,
물고기가 실제로 이동하는 방향
을 그대로 물고기의 방향(facing)으로 사용했습니다.
평소에 헤엄치는 물고기라면 이것만으로도 문제가 없습니다.
오른쪽으로 나아가는 물고기는 오른쪽을 바라보고.
왼쪽으로 나아가는 물고기는 왼쪽을 바라봅니다.
하지만 낚시에서는 깨집니다.
플레이어가 릴을 감으면,
물고기가 줄에 의해 앞쪽으로 끌려감
↓
물고기의 좌표가 앞쪽으로 이동함
...
이 됩니다.
즉,
필사적으로 도망치고 있어야 할 물고기가 스스로 우리 쪽으로 헤엄쳐 오는 것.
이 엄청나게 이상했습니다.
그래서,
순수 이동(Net movement)
과
물고기 자체의 자력 유영(Self-powered swimming)
을 분리했습니다.
export interface FishMotionSnapshot {
position: Vec3;
velocity: Vec3;
...
}
이렇게 해서,
실제 이동
→ 플레이어 측
물고기 자신이 헤엄치는 방향
...
을 동시에 가질 수 있게 되었습니다.
결과적으로,
줄의 힘으로 앞쪽으로 끌려가는데, 물고기 자신은 바다 저 멀리(out) 계속 도망치는
표현이 가능해졌습니다.
이것은 상당히 효과적이었습니다. 모델 자체는 거의 바뀌지 않았음에도 불구하고,
'객체가 운반되고 있다'
에서
'생명체가 저항하고 있다'
로 보이는 방식이 달라집니다.
다음은 회전(turning)입니다.
파이트 중에는 물고기가 좌우로 도망칩니다. 처음에는 모드별로 사인파를 사용해 좌우 이동시켰습니다.
다만,
rest
↓
surge
처럼 상태가 바뀐 순간, 위상(phase)이나 목표 방향까지 전환하면,
쿵
하고 물고기가 반대 방향으로 움직입니다. 생명체라기보다는 NPC 같습니다.
그래서,
- 횡방향 위상을 연속적으로 적분(integrate)
- 횡방향 가속도에 상한선 설정
- 회전 속도에도 상한선 설정
을 두었습니다. 상태 자체는 **'오른쪽으로 도망치고 싶다'**로 순식간에 바뀌어도,
몸은 그곳에서 점진적으로 오른쪽으로 구부러집니다.
이
의지는 순간적으로 변해도, 몸은 순간적으로 바뀔 수 없다
는 느낌이 꽤 중요했습니다.
논문뿐만 아니라 GitHub에 있는 물고기나 생물의 Procedural Animation도 AI에게 조사하게 했습니다. 설계 시 참고한 것은 예를 들어 다음과 같은 구현입니다.
animal-proc-anim
- Google WebGL Aquarium
- Three.js GPGPU Birds
- PolyFish
여기서 참조한 것은 설계 패턴이나 생각입니다. 물고기의 메쉬, 텍스처, 제3자 코드를 그대로 복사한 것이 아닙니다.
이 구현에서는 물고기를 하나의 척추(spine) 체인으로 다루고 있습니다. 척추의 각 위치에 신체 폭을 부여하여,
중심선이 구부러지면 → 몸과 지느러미가 그 뒤를 따라온다
는 생각입니다.
기술 낚시에서는 2D 체인 자체는 사용하지 않고,
3D 메쉬 + 정점 셰이더(Vertex Shader)
로 재구축했습니다. WebGL Aquarium에서는 물고기 몸체의 후방일수록 변형량을 크게 하여,
머리는 비교적 안정시키고, 꼬리로 갈수록 강하게 흔드는
방식이 사용되었습니다.
이 생각 자체는 기술 낚시에서도 사용하고 있습니다. 다만, 일반적인 수족관과 낚시 게임에는 하나의 큰 차이가 있습니다.
수족관이라면,
진행 방향 ≒ 물고기가 헤엄치는 방향
으로 거의 문제가 없습니다. 낚시는,
진행 방향 ≠ 물고기 자신이 헤엄치는 방향
이 됩니다.
기존 패턴을 그대로 사용하는 것이 아니라,
낚시라는 외력이 있는 상황에 맞춰 재구성할 필요가 있었습니다.
여기는 꽤 재미있었습니다.
Go물고기의 3D 모델도 직접 만들었습니다.
GLB 같은 완성된 물고기 모델을 불러오는 것이 아닙니다.
먼저, 물고기 몸통의 단면을 정의합니다.
const profile = [
[-1.86, .018, .016, -.012],
[-1.67, .19, .12, .016],
...
이것을 CatmullRomCurve3로 연결하여,
각 위치에 원주형의 정점(頂点)을 생성하고, BufferGeometry로서 물고기 몸통을 만들었습니다.
그 외에도,
- 등지느러미 (背びれ)
- 가슴지느러미 (胸びれ)
- 배지느러미 (腹びれ)
- 꼬리지느러미 (尻びれ)
- 두 갈래의 꼬리
- 눈
- 입
- 아가미
- 발광 노드
등을 만들었습니다.
모델 설계에서는,
머리는 뾰족하게
몸통은 제대로 두껍게
꼬리 자루는 가늘게
발광이 너무 과하지 않게
등도 결정했습니다.
폴리곤 수를 늘리기보다는,
물고기로 인식되는 실루엣
을 우선시했습니다.
모델을 그대로 좌우로 회전시켜도, 일단 헤엄치는 것처럼 보입니다.
하지만 상당히
장난감 같은 느낌이 있습니다.
그래서 정점 셰이더(頂点シェーダー) 측에서 물고기 전체에 연속적인 파동을 흐르게 했습니다.
float bendZ(float x) {
float s = clamp(
(x + 1.45) /
...
핵심은 * s * s입니다.
머리 쪽에서는 변형을 작게 하고,
뒤로 갈수록 진폭을 크게 했습니다.
게다가 단순한 시간뿐만 아니라,
uTurn
uEffort
uTetherLoad
uSwimPower
uNaturalSwim
등을 셰이더로 전달했습니다.
즉,
같은 물고기 모델에 고정된 수영 애니메이션을 재생하는 것이 아닙니다.
그 순간의 물고기 상태에 따라 몸의 움직임이 바뀝니다.
이것도 상당히 재미있었습니다.
처음에는 지느러미도 몸통 파동에 그대로 실어 놓았습니다.
그러자,
물고기라기보다는 물속에서 주머니를 잡아당기는 느낌이 되었습니다.
그래서 현재는, 몸통과 지느러미의 반응을 약간 분리했습니다.
예를 들어 강하게 헤엄칠 때는 등지느러미를 접고 꼬리의 모양을 바꿉니다.
반대로 저속일 때나 선회할 때는 가슴지느러미 같은 대지느러미에 작은 움직임을 더합니다.
if (p.x > 1.6) {
// tail
p.y *= 1.0 - response * uEffort * .22;
...
이 부분도,
논문의 물고기를 그대로 재현하고 있는 것은 아닙니다.
다만 실제 물고기 연구를 봐도,
모든 지느러미가 같은 주기로 단순히 흔들리는 것은 아니다
는 것을 알 수 있습니다.
그것을 게임 표현에 녹여냈습니다.
조사하는 도중에 재미있었던 것이,
물고기는 꼬리만으로 나아가는 것이 아니다
라는 이야기입니다.
Lucas 등이 진행한 연구에서는, 물고기 몸통의 전방 부분도 압력 차이에 의해 추력에 기여할 수 있음이 제시되었습니다.
물론 기술 낚시에서는 유체역학을 계산하고 있지는 않습니다.
그래서,
공기 중의 카메라
↓
물결이 있는 수면
...
이러한 구성을 취하고 있습니다.
따라서 논문의 모델을 그대로 구현한 것은 아닙니다.
단지,
수중의 빛을 어떻게 분리하여 생각할 것인가
에 대한 참고 자료로 활용했습니다.
기술적인 측면에서는 물의 굴절률로 1.333을 참고하고 있습니다.
const cosWater = Math.sqrt(
1 -
(1 - cosAir * cosAir) /
...
물고기가 수중으로 들어갈 경우, 단순히 위치를 투영하는 것이 아니라,
수면 너머로 봤을 때의 겉보기 깊이를 근사하고 있습니다.
게다가 셰이더(shader) 측에서는 Fresnel 효과도 계산했습니다.
float rs =
(cosAir - 1.333 * cosWater) /
(cosAir + 1.333 * cosWater);
...
수면에 대해 낮은 각도에서 볼수록 반사가 강해지고,
수중에서 도달하는 빛은 줄어듭니다.
RGB 채널에 대해서도 같은 비율이 아니라,
vec3 extinction =
exp(-vec3(.58, .22, .17) * path);
처럼 개별적인 감쇠(attenuation)를 적용했습니다.
여기가 상당히 중요합니다.
코드에도 다음과 같이 주석을 달았습니다.
Art-directed coherent refraction,
not a full refracted-ray renderer.
즉,
완벽한 굴절 광선 추적(full refracted-ray tracking)은 아닙니다.
처음에는 물결의 높이를 물고기 신체 각 정점의 겉보기 위치에 강하게 반영했습니다. 그러자,
물고기가 구불구불해집니다.
지느러미도 꼬리도 천 조각처럼 됩니다.
당연합니다. 수면의 물결 때문에 **보이는 모습(見え方)**이 바뀌어야 하는데,
화면상에서 물고기 자체가 변형되는 것처럼 보였으니까요.
그래서 물고기 한 마리에 공통적인 광학적 중심을 사용하고,
물고기 자체의 형태는 최대한 유지하는 방향으로 변경했습니다.
게다가,
Math.max(.55, ...)
같은 하한값이나,
(world.y - center.y) * .72
같은 보정치도 넣었습니다.
이 0.55나 0.72는 물리 상수가 아닙니다.
물고기의 형태를 망가뜨리지 않으면서 수면 너머로 보이게 하기 위한 조정값입니다.
여기서 가장 재미있었던 부분이었습니다.
굴절을 넣는다.
Fresnel 효과를 넣는다.
거리와 깊이에 따라 감쇠시킨다.
RGB 채널도 개별적으로 감쇠시킨다.
수면 배경과 제대로 합성한다.
'좋아, 꽤 수중 같은 느낌이 들었어'라고 생각하며 실제 게임을 실행했습니다.
물고기가 안 보여요.
근거리에서 옆으로 보는 테스트에서는 확실히 물고기가 있습니다.
하지만 실제 낚시 카메라 시점에서는,
물고기를 거의 인식할 수 없습니다.
여기서,
'그려지고 있다(描画されている)' 것과
**'플레이어에게 보인다(プレイヤーから見える)'**는 것이 완전히 다르다는 것을 깨달았습니다.
그래서 최종적으로, 광학적 감쇠와 별개로
최소한 물고기 몸체가 배경을 얼마나 가리는가
라는 값을 부여했습니다.
const presence = {
floor: .30,
surface: .32,
...
코드 내에서는 이것을
artistic readability budget (예술적 가독성 예산)
으로 취급하고 있습니다.
즉,
읽기 위한 예산입니다.
몸통은 비교적 남기고.
지느러미는 조금 옅게 하고.
세밀한 발광 부분은 더 약하게 합니다.
다만 깊어지거나 멀어지면 제대로 사라지게 합니다.
이렇게 함으로써,
수중 같은 느낌과
물고기가 존재함을 동시에 달성했습니다.
여기서 생각의 방식이 크게 바뀌었습니다.
실사 게임 그래픽은
물리적으로 올바른 값들을 나열한다고 완성되는 것이 아니다.
오히려,
인간이 그 화면을 어떻게 읽어내는지까지 포함해야 실사적이라고 생각합니다.
Go 물고기에는 시안(cyan) 발광 표현이 있습니다. 처음에는
'기술적인 느낌의 물고기라면 빛나는 부분이 많을수록 멋있겠지' 정도였습니다.
하지만 수중에 넣자,
어두운 몸통보다 발광선만 눈에 띄어서,
빛나는 실밥 같아졌습니다.
그래서 AI에게 발광하는 물고기에 대해서도 조사하게 했습니다. Martin 등이 수행한 하다카이와시류의 발광기 연구에서는,
발광 부분은 신체 전체가 아니라, 발광 조직/색소층/반사 구조 등을 가진 국소적인 기관으로 존재하고 있습니다.
물론 Go 물고기는 가상의 생물입니다. 하다카이와시의 발광 구조를 충실히 재현한 것도 아닙니다. 참고한 것은,
불투명한 몸체 + 국소적 발광이라는 구성입니다.
그래서,
- 어두운 몸체는 제대로 남기고
- 발광은 국소적으로 하고
- 수중에서는 가는 발광선을 억제하는
방향으로 했습니다.
물고기뿐만 아니라 바다 자체도 상당히 건드렸습니다.
Three.js의 WebGLRenderer
위에서,
직접 만든 ShaderMaterial
을 사용해서 바다를 그리고 있습니다.
중에는,
- 절차적(procedural) 하늘
- 다중 주파수 파도
- 하늘 반사
- Fresnel
- 태양의 글린트(glint)
- 착수 물결
- 원경 헤이즈(haze)
- 비네팅(vignette)
등을 모아 넣었습니다.
float fresnel =
0.035 +
0.965 *
...
README에는 처음부터,
'물고기나 설명이 없어도 보고 싶은 바다'
라고 적어두었습니다.
그래서,
물고기가 없는 상태에서도 성립하는 풍경
을 상당히 의식했습니다.
물고기만 아무리 자연스럽게 움직여도,
물고기만 멋대로 날뛰면 낚시처럼 보이지 않습니다.
결국에는,
물고기
↓
줄
...
까지 같은 힘이 흐르는 것처럼 보이게 해야 합니다.
줄에 대해서도,
낚싯대 끝
↓
수면 진입점
...
까지 제대로 연결했습니다.
예전에는 줄이 수면 위의 보이지 않는 지점에 연결되어 있어서,
'결국 뭘 당기고 있는 거야?'
라는 시각으로 보이곤 했습니다.
물고기 입에 연결하여서야 비로소
물고기를 끌어당기고 있다
라고 읽을 수 있습니다.
낚싯대도 하나의 검은 선이 아니라,
- 가늘어지는 블랭크(blank)
- 가이드
- 릴
- 휘어짐(しなり)
- 재질의 반사
까지 Three.js로 만들었습니다.
게다가, 물고기・줄・낚싯대의 세밀한 연출에는 같은 수영 위상을 사용하는 부분도 있습니다.
다만,
물고기・유체・줄・낚싯대를 연결한 물리 시뮬레이션
은 아닙니다.
단지,
화면상에서 '힘이 연결되어 있다'고 읽을 수 있게 하는 연출
입니다.
하지만 이것이 들어가면서,
저 자신에게는 처음으로
'물고기가 움직이고 있다'
에서
'물고기를 낚고 있다'
로 바뀌었습니다.
나중에 커밋 기록을 되돌아보면 상당히 명확합니다.
처음에 바다와 물고기를 만들고 끝내는 것이 아니라,
바다와 물고기를 만들기
↓
물고기 이동을 서버와 동기화하기
...
처럼, 후반부 커밋일수록
'움직이는가'보다 '어떻게 느껴지는가'
에 치우쳐 있습니다.
특히 물고기에 대해서는,
구현했다 → 위화감을 발견했다 → 원인을 조사했다 → 수정했다
를 여러 번 반복했습니다.
이번 글 자체도, 완성된 코드만 보는 것보다,
커밋의 변천을 보는 것이 무엇에 공들였는지 알기 쉬웠습니다
같습니다.
이번에 개인적으로 가장 많이 배운 것은 아마 여기일 겁니다.
생성 AI를 사용하면,
코드를 생성하게 하는 것
에 눈이 가기 쉽습니다.
물론 이번에도 구현에는 상당히 AI를 사용했습니다.
하지만 가장 도움이 된 것은 오히려,
'무엇이 이상한지'를 함께 생각하게 한 것
이었습니다.
예를 들어,
물고기가 쓰레기 같다
라는 감상만으로는 그대로 구현에 옮길 수 없습니다.
거기서 AI에게,
방향이 이상한가?
신체 변형이 이상한가?
지느러미가 이상한가?
...
로 분해하게 합니다.
각각에 대해 관련 연구를 찾아보게 하고.
OSS에서 다른 사람들이 어떻게 구현했는지 조사하게 합니다.
자신의 코드와 비교합니다.
그 위에서,
어떤 것을 채택할지는 실제 화면을 보고 결정합니다.
이 루프가 상당히 강력했습니다.
반면에, 이번에 매우 중요했던 것이 이것입니다.
결국은 실화면으로 본다.
AI가,
'이 처리로 자연스러워질 겁니다'라고 말해도,
브라우저를 열어보면 평범하게
물고기가 안 보여요
같은 일이 일어납니다.
논문에 가까운 처리를 넣었더라도,
실제 카메라・물고기 크기・배경・화면 사이즈에서는 성립하지 않을 때가 있습니다.
그래서 이번에는,
AI의 분석
↓
구현
...
을 상당히 반복했습니다.
이
'AI의 답'보다 '움직이는 화면'을 우선하는 것
은 그래픽스 계열에서는 특히 중요하다고 생각했습니다.
이번에 가장 느낀 것은,
리얼함은 하나의 고정밀한 시뮬레이션에서 태어나는 것이 아니라는 점입니다.
물고기 모델이 아무리 아름다워도,
플레이어를 향해 도망가면 이상합니다.
물리적인 굴절이라 해도,
물고기가 보이지 않으면 게임으로서 이상합니다.
아름다운 꼬리 애니메이션이라도,
줄과 낚싯대가 무반응이면 낚시처럼 보이지 않습니다.
반대로,
- 물고기 자체가 바다로 도망가고
- 낚싯줄의 힘으로 실제 위치는 앞으로 온다
- 상태에 따라 헤엄치는 방식이 달라진다
- 몸체와 지느러미가 각각 반응한다
- 수중에서는 제대로 감쇠된다
- 하지만 물고기 몸체는 인식할 수 있다
- 낚싯줄이 입과 연결된다
- 낚싯대가 그 힘에 반응한다
- 수면에도 잔물결이 생긴다
같은 작은 단서들이 모순 없이 이어지면,
갑자기
'물고기가 저기에 있다'
라는 느낌이 생겨납니다.
이번에 진행한 것은 결국,
실제 물고기를 시뮬레이션하는 것
이라기보다는,
'물고기를 낚고 있다'는 느낌을 받기 위한 모순들을 하나씩 제거해 나가는 과정
이었는지도 모릅니다.
기술적으로는,
물고기의 3D 모델을 Three.js로 직접 만들었습니다.
물고기의 움직임 또한 Procedural Animation으로 제작했습니다.
이동과 자력 유영(self-propelled swimming)을 분리했습니다.
수면 너머의 굴절이나 Fresnel 효과도 적용했습니다.
물리적으로 구현한 결과, 물고기가 잘 보이지 않게 되어 가시성 조정도 했습니다.
물고기・낚싯줄・낚싯대・수면까지 연결했습니다.
그리고 그 과정에서 AI에게,
물고기의 유영 연구
수중 광학(Underwater Optics)
발광 물고기 연구
GitHub상의 선행 구현 사례
를 교차 조사하게 했습니다.
연구나 OSS(Open Source Software)를 그대로 재현하는 것이 아니라,
연구・선행 구현
↓
활용 가능한 아이디어를 추출
...
이런 방식으로 활용했습니다.
처음에는
'스마트폰을 낚싯대로 만들면 재미있지 않을까?'
정도에서 시작한 게임입니다. 어느새 AI에게 물고기의 근육 활동과 수중 광학 논문을 읽게 하고 있었습니다.
낚시 게임, 깊이가 있습니다.
이번 조사에서 특히 참고한 자료들입니다.
-
Flammang & Lauder,
Speed-dependent intrinsic caudal fin muscle recruitment during steady swimming in bluegill sunfish, 2008 - Flammang & Lauder,
Caudal fin shape modulation and control during acceleration, braking and backing maneuvers in bluegill sunfish, 2009 - Lucas, Lauder & Tytell,
Airfoil-like mechanics generate thrust on the anterior body of swimming fishes, 2020 -
Akkaynak & Treibitz,
A Revised Underwater Image Formation Model, CVPR 2018 -
Martin et al.,
Variation in lanternfish photophore structure: A comprehensive comparative analysis, 2024 -
argonautcode / animal-proc-anim
-
Google WebGL Aquarium
-
Three.js GPGPU Birds
-
PolyFish
각 연구에 대해서는 '이 게임이 연구 내용을 정확히 재현하고 있다'는 의미가 아니라, 구현 방식을 구상하는 참고 자료로 활용했습니다.
특히 수중 표현에는 가시성을 우선한 독자적인 보정값이 포함되어 있습니다. 발표에서는 이 모든 것을 다 이야기할 시간이 없어서,
'물고기가 제대로 생명체처럼 움직인다'
정도만 요약했습니다.
하지만 내용을 깊이 파고들면, 생각했던 것보다 훨씬 많은 작업을 했습니다.
구현 과정을 되짚어보며 글을 쓰는 것이 정말 재미있습니다. 다음에는,
스마트폰을 어떻게 낚싯대로 만들었는지
라든가,
WebSocket으로 PC와 스마트폰을 어떻게 동기화했는지
정도도 다룰 수 있으면 좋겠습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기