개구리를 마우스로 끌면 몸통이 물렁하게 눌렸다가 제 모양으로 돌아오고, 팔다리는 그 움직임을 따라 스스로 자세를 잡습니다.
프론트엔드 개발자로서 게임 엔진이나 물리 시뮬레이션 라이브러리에 관심이 많았지만 직접 구현해볼 기회는 별로 없었습니다. 그러던 중 GitHub에서 소프트 바디 물리 엔진으로 만든 개구리 애니메이션을 우연히 발견했습니다. 겉보기에는 단순한 장난감 같지만 실제로 작동하는 물리 엔진이었고, 이를 분석해 다른 플랫폼으로 옮기는 작업은 생각보다 만만치 않았습니다.
원래 Processing 코드가 보여준 가능성
소프트 바디 물리가 만드는 현실감
기존의 개구리 애니메이션은 대부분 뼈대 구조를 기반으로 한 리깅(Rigging) 방식을 사용합니다. 하지만 이 프로젝트는 완전히 다른 접근법을 택했습니다. 개구리의 몸통을 수십 개의 점으로 구성된 '블롭(Blob)'으로 만들고 이 점들이 서로 연결되어 물리적인 상호작용을 하는 구조입니다.
class Blob {
ArrayList<BlobPoint> points;
Blob(PVector origin, int numPoints, float radius, float puffiness) {
points = new ArrayList<BlobPoint>();
for (int i = 0; i < numPoints; i++) {
PVector offset = new PVector(
cos(TWO_PI * i / numPoints - HALF_PI) * radius,
sin(TWO_PI * i / numPoints - HALF_PI) * radius
);
points.add(new BlobPoint(PVector.add(origin, offset)));
}
}
}이 블롭은 단순한 모양이 아닙니다. 베를레 적분(Verlet Integration)을 사용한 물리 엔진이 각 점에 중력을 적용하고 인접한 점들 사이의 거리를 유지하며 부드러운 변형을 만들어냅니다.
팔다리의 유기적인 움직임
개구리의 팔다리는 두 개의 관절로 구성된 역운동학(Inverse Kinematics, IK) 체계를 사용합니다. 몸통의 앵커 포인트(Anchor Point)에서 시작해서 팔꿈치와 발끝까지 이어지는 체인 구조입니다.
class Limb {
LimbPoint elbow;
LimbPoint foot;
Limb(PVector origin, float distance, float elbowRange, float elbowOffset,
float footRange, float footOffset) {
elbow = new LimbPoint(PVector.add(origin, new PVector(0, distance)));
foot = new LimbPoint(PVector.add(elbow.pos, new PVector(0, distance)));
}
}특히 인상적인 부분은 땅에 닿았을 때의 반응입니다. 발이 땅에 가까워지면 자연스럽게 자세를 교정하는 로직이 들어 있어서 실제 동물처럼 보입니다.
React 환경으로 포팅한 과정
Processing의 웹 버전, p5.js
Processing 코드를 웹에서 실행하기 위해 선택한 도구는 p5.js였습니다. Processing의 JavaScript 버전인 p5.js는 동일한 API를 제공하면서도 브라우저 환경에 최적화되어 있습니다.
// React 컴포넌트에서 p5.js 사용
const sketch = (p: p5) => {
let frog: Frog;
p.setup = () => {
p.createCanvas(800, 600);
frog = new Frog(new p5.Vector(p.width / 2, p.height - 256));
};
p.draw = () => {
p.background(40, 44, 52);
frog.update(p);
frog.display(p);
};
};
const p5Instance = new p5(sketch, containerRef.current);호환성 문제와 해결 과정
포팅에서 가장 힘들었던 부분은 p5.js 버전이 달라지면서 바뀐 API였습니다. 원래 Processing 코드는 curveVertex()로 부드러운 곡선을 그렸지만 p5.js v2에서는 이 함수가 제대로 작동하지 않았습니다.
// 원래 Processing 코드
beginShape();
curveVertex(points.get(0).x, points.get(0).y);
curveVertex(points.get(1).x, points.get(1).y);
// ... 더 많은 curveVertex 호출
endShape();
// 수정된 p5.js 코드
beginShape();
for (const point of points) {
vertex(point.pos.x, point.pos.y);
}
endShape(CLOSE);결국 vertex()를 쓰는 폴리곤(Polygon) 렌더링으로 바꿨는데 오히려 이 방식이 더 간단하고 성능도 좋았습니다.
기술적 세부사항과 배운 점
물리 엔진의 뼈대, 베를레 적분
베를레 적분은 위치 기반의 물리 시뮬레이션 방법입니다. 기존의 속도-가속도 기반 방식과 달리 이전 위치와 현재 위치만으로 다음 위치를 계산합니다.
void verletIntegrate() {
PVector temp = pos.copy();
PVector vel = PVector.sub(pos, ppos).mult(0.99); // damping
pos.add(vel);
ppos = temp;
}이 방식의 장점은 안정성과 단순성입니다. 복잡한 미분 방정식을 풀 필요 없이 직관적인 위치 기반 제약조건으로 자연스러운 움직임을 만들 수 있습니다.
다중 제약 조건 해결
블롭의 부드러운 변형을 위해 세 가지 제약 조건을 동시에 해결합니다:
- 거리 제약: 인접한 점들 사이의 거리를 유지
- 부피 제약: 블롭의 전체 면적을 일정하게 유지
- 충돌 처리: 경계와 마우스 상호작용
충돌 처리 부분이 특히 눈에 띄었습니다:
void collideWithMouse() {
PVector mouse = new PVector(mouseX, mouseY);
if (mousePressed && PVector.dist(pos, mouse) < 100) {
PVector diff = PVector.sub(pos, mouse).setMag(100);
pos = PVector.add(mouse, diff);
}
}마우스로 블롭을 밀면 실제 물체처럼 반발합니다.
성능 최적화와 실시간 렌더링
60fps 유지하기 위한 최적화
실시간 애니메이션을 위해 여러 최적화 기법을 적용했습니다:
- 반복 횟수를 10회로 제한한 수렴 알고리즘
- 선택적 렌더링 (필요한 부분만 다시 그림)
- 효율적인 벡터 연산
크로스 플랫폼 호환성
Processing에서 p5.js로, 다시 React 컴포넌트로 옮기면서 플랫폼에 중립적인 설계가 왜 중요한지 실감했습니다. 물리 엔진 로직은 플랫폼에 종속되지 않도록 분리하고 렌더링만 각 플랫폼에 맞게 조정했습니다.
마치며
이 소프트 바디 개구리는 물리 시뮬레이션이 어디까지 갈 수 있는지 잘 보여준 사례였습니다. 게임 엔진처럼 복잡해 보이는 시뮬레이션도 베를레 적분과 제약 조건, 충돌 감지 같은 기본 원리와 반복적인 최적화만으로 충분히 만들 수 있었습니다. 이 개념들은 게임은 물론 UI 애니메이션이나 데이터 시각화에도 그대로 옮겨 쓸 수 있습니다.
Processing에서 시작해 p5.js를 거쳐 React로 옮기는 과정도 그 자체로 배움이었습니다. 물리 로직은 플랫폼에 종속되지 않게 분리하고 렌더링만 각 환경에 맞추니 p5.js 버전이 바뀌며 API가 달라지는 문제도 한결 다루기 쉬웠습니다. GitHub에서 찾은 구현을 뜯어보며 다른 개발자의 접근을 배운 것도 큰 몫이었습니다.
물리 시뮬레이션이나 크로스 플랫폼 개발에 관심 있는 분들에게 작은 영감이 되었으면 좋겠습니다. 개구리가 마우스 클릭에 어떻게 반응하는지 직접 확인해 보세요.
