BHNB
bhnb.havana.moe
지난번 개발했던 별 헤는 밤에 랜덤하게 유성이 떨어지는 기능을 추가로 구현하였습니다. 개발 과정은 아래와 같습니다.
개발 과정
유성을 그리기 위해서는 무엇을 해야 할까요? 우선 유성이 어떻게 떨어지는지를 생각해봐야 합니다. 실제로 밤하늘의 유성을 보신 분들이 얼마나 있을지 모르겠지만, 직접 본 것이 아니더라도 미디어 등에서 한 번쯤은 봤을 것이라고 생각합니다. 유성은 밤하늘에 하얀 선이 그려졌다가 서서히 사라지는 방식으로 우리 눈에 보입니다. 아래는 그러한 유성을 three.js로 유사하게 구현하기 위한 과정입니다.
선 그리기
앞서 말했듯이 유성은 밤하늘에 하얀 선이 그려진다고도 생각할 수 있습니다. 실제로 밤하늘 사진을 찍다 보면 운 좋게 유성이 같이 찍힐 때가 있는데, 하얀 선처럼 찍히고는 합니다. 이를 구현하기 위해 우선 밤하늘에 선을 그어보기로 했습니다.
선을 그리기 위해 line이라는 mesh를 사용하였습니다. 유성이 떨어지는 원리를 생각해보면, 떨어지는 별을 하나 그리고 trail을 그리는 방식으로도 구현할 수 있겠지만, 실제 밤하늘에 보이는 모습과는 괴리가 있고 (우주 멀리서 떨어지다 보니 그러한 디테일을 관측하기 쉽지 않습니다) 성능 상의 이슈도 있을 것이라고 생각해서 line으로 구현하기로 하였습니다.
유성을 그려주는 컴포넌트 Comet을 만들고, 미리 정의된 lineCount 수 만큼의 Comet을 그려주었습니다. 각각의 Comet에는 아래의 코드를 이용해 유성의 3차원 위치, 시작점, 그리고 끝점을 무작위로 설정해주었습니다.
const x = (Math.random() * SCALE + SCALE / 2) * (Math.random() - 0.5) * 2;
const y = (Math.random() * SCALE + SCALE / 2) * (Math.random() - 0.5) * 2;
const z = (Math.random() * SCALE + SCALE / 2) * (Math.random() - 0.5) * 2;
const dx = (-Math.random() * SCALE) / 10 + x;
const dy = (-Math.random() * SCALE) / 10 + y;
const dz = (-Math.random() * SCALE) / 10 + z;
const ddx = (Math.random() * SCALE) / 10 + x;
const ddy = (Math.random() * SCALE) / 10 + y;
const ddz = (Math.random() * SCALE) / 10 + z;
중심이 원점이고 반지름이 $SCLAE / 2$ 인 구와 중심이 원점이고 반지름이 $SCALE + SCALE / 2$ 인 구 사이에 유성의 위치를 설정해주고, 해당 점으로부터 $x, y, z$ 값이 각각 최대 $SCALE / 10$ 만큼 떨어진 시작점과 끝점을 구해주었습니다.
이렇게 구한 위치를 bufferAttribute를 사용해 position으로 설정해줍니다. 이후 bufferGeometry의 setFromPoints 메소드를 사용하여 시작점과 끝점, 두 점으로 이루어진 array를 넣어주면 선을 그릴 수 있습니다. material에서 색을 흰색으로 설정해주면 선 그리기는 거의 끝났다고 할 수 있습니다.
선 움직이기
아직은 뭔가 어색합니다. 우주 공간 위에 하얀 선 15개가 가만히 멈춰있습니다. 유성은 이렇지 않습니다. 유성은 (당연하게도) 움직입니다. 정확히는 시작점부터 시작해서 서서히 선이 그려지며, 시작점부터 시작하여 다시 서서히 선이 사라집니다. 이러한 애니메이션은 어떻게 구현할 수 있을까요?
3D에서는 어떨지 모르겠지만, 2D에서는 로고 형태를 따라 선이 그어지는 등 꽤나 자주 보이는 애니메이션입니다. 2D에서는 어떻게 구현되어 있을지 찾아본 결과, css에서는 dashed 속성을 이용하여 선이 그어지는 애니메이션을 표현한다는 것을 알게 되었습니다. gap size(dashed line의 빈 부분)를 엄청 크게 설정한 다음 dash size(dashed line의 칠해진 부분)를 조금씩 늘려나가면 선이 그려지는 듯한 표현이 가능해요! 진짜 정말 너무 엄청 신기하지 않나요? 처음 알았을 때 너무 신기해서 주변에 다섯 번 쯤 말한 것 같습니다.
그렇다면 이를 3D에 어떻게 적용할 수 있을까요? 정말 다행이게도 three.js에는 lineDasheMaterial이라는 좋은 material이 있습니다. gap size와 dash size를 설정할 수 있으므로, 매 프레임마다 dash Size를 조금씩 늘려주면 될 것 같습니다. 될 것 같았...는데...
아무런 일도 일어나지 않았습니다. 그냥 하얀 선이 움직이지 않고 그려질 뿐입니다. 왜일까요? 구글링 해 본 결과 찾은 정답은 computeLineDistances 메소드를 호출하지 않았기 때문입니다. 생각해보면 당연한 일입니다. Line distance를 알아야 dashed line을 그릴 수 있을 것 같습니다.
그런데 여기서 문제가 발생했습니다. react-three-fiber에서의 몇몇 컴포넌트 (line, path, mesh 등)이 svg elements와 conflict가 나는 것입니다. TypeScript가 three.js의 line을 svg element의 line으로 인식하여 타입이 SVGLineElement로 인식되고, 따라서 SVGLineElement에는 computeLineDistances 메소드가 존재하지 않기 때문에 해당 메소드를 호출할 수 없는 문제가 생겼습니다. 이를 해결하기 위해 위 링크에 적힌 방법을 따라 Line_ 클래스를 추가하는 방식으로 해당 문제를 해결하였습니다. 이 문제를 파악하고 해결하는데 꽤 오랜 시간이 걸렸는데, 해결하고 나니 뿌듯한거 같기도 하고 그렇습니다.
// Add class `Line` as `Line_` to react-three-fiber's extend function. This
// makes it so that when you use <line_> in a <Canvas>, the three reconciler
// will use the class `Line`
extend({ Line_: Line });
// declare `line_` as a JSX element so that typescript doesn't complain
declare global {
namespace JSX {
interface IntrinsicElements {
line_: ReactThreeFiber.Object3DNode<Line, typeof Line>;
}
}
}
여기까지 했으면 이제 유성이 서서히 그려지는 효과를 볼 수 있을 것입니다. 그러나 여기서 끝이 아닙니다. 유성이 그려지는 만큼, 유성이 사라지는 것 역시 구현해야 합니다. css에서는 해당 방법을 gap offset을 이용하여 구현하는 것 같은데, 안타깝게도 three.js의 lineDashedMaterial에서 gap offset에 대응되는 attribute를 찾지는 못했습니다. 혹시 알고 계신 분이 있다면, 댓글로 알려주시면 감사하겠습니다.
gap offset의 대체제로 찾은 방법은 line의 길이 자체를 바꾸는 것입니다. 유성이 다 그려지면, 시작점의 위치를 조금씩 끝점의 위치와 가깝게 업데이트하면 유성이 서서히 사라지는 효과를 구현할 수 있습니다. 매 프레임마다 변하는 시작점의 위치는 Vector3에 있는 lerp라는 메소드를 통해 구할 수 있습니다. 이렇게 프레임마다 시작점의 위치를 바꾸면 유성이 사라지는 효과까지 구현 완료입니다.
여기서 드는 생각은... 유성이 그려지는 것도 이렇게 구현하면 안 되냐는 거긴 한데, 그 편이 일관성 있고 좋을 것 같지만 열심히 SVG element와의 conflict를 고치던 시간이 아깝기도 하고 dahsed line을 이용하는 방법이 너무 흥미로워서 두 가지 방법을 섞어서 구현하였습니다. line의 geometry를 업데이트 하는 것 보다 material 단에서 처리해주는 것이 성능 상으로 더 좋지 않을까? 하는 기대도 있었습니다. (실제로 그런지는 잘 모릅니다.)
무작위 유성
여전히 어색한 부분이 존재합니다. 유성이 맨 처음 한 번만 그려지고 말기 때문입니다. 이를 개선하기 위해 CYCLE이라는 값을 설정하고, 해당 시간마다 유성이 한 번씩 그려지고 사라지게 구현하였습니다. 그러나 유성이 계속 같은 위치에서 떨어집니다. 조금 이상하지 않나요? 실제 별을 보러 갈 때도, 어디에 떨어질지 모른다는 점이 유성의 매력입니다.
유성을 랜덤한 위치에 떨어뜨리기 위해 유성의 값들을 초기화하는 함수 initialize를 만들고 CYCLE만큼이 시간이 지날 때마다 initialize 함수를 호출하는 방법으로 구현하였습니다. 이제 실제 밤하늘과 비슷하게 유성이 무작위 위치에 무작위 방향으로, 무작위 길이만큼 떨어집니다. 밤하늘을 멍하니 바라보는 것을 노트북 앞에서도 할 수 있습니다.
깃헙 레포지터리
https://github.com/havana723/bhnb-opengl
GitHub - havana723/bhnb-opengl
Contribute to havana723/bhnb-opengl development by creating an account on GitHub.
github.com
사용된 코드는 전부 위 레포에 있으므로, 관심 있으신 분들은 한 번 씩 확인해주시면 감사하겠습니다.
+) 아래는 동아리 내부 스터디에서 발표 자료로 사용했던 슬라이드입니다.
'개발 > 토이 프로젝트' 카테고리의 다른 글
별 헤는 밤 BHNB 알파 버전 개발 후기 (0) | 2022.09.28 |
---|---|
내가 월드 파이널에 갈 수 있을 리 없잖아, 무리무리! (※무리가 아니었다?!) (0) | 2022.08.30 |