Flutter 3D 애니메이션 활용기

Devil😈·2024년 1월 21일

Flutter

목록 보기
1/1

왜 시작했을까

노마드코더 플러터 10주 스터디 1기 애니메이션 마지막과제로 art 갤러리를 만들었었다.
그림을 배경과 분리 시킨다음 애프터이펙트로 움직임을 주고 프레임의 기울기와 Offset 차이를 이용해서 살짝 3D로 보이게 하는 효과였다.

아래 그림 참조
이미지 링크는 여기

일종의 Fake 3D라고 할 수 있는데 2기 마지막 애니메이션 과제에서는 제대로된 3D를 한 번 사용해보고 싶었다.
하고 싶으면 해봐야지.

o3d 패키지

3D 모델을 Flutter로 제대로 표현하려면 아무래도 Flame 3D같은 게임 라이브러리를 쓰는게 가장 효과적이지 않을까 생각했지만 간단한 졸업과제에서 맛만 볼거라서 o3d 패키지를 사용했다

O3D 링크

패키지 소개에 써있듯이 glTF와 GLB 파일을 WebView와 model-viewer를 통해서 화면에 띄워주는 역할을 한다. 즉, 앱 내부에서 모델링을 해주는게 아니라 웹뷰를 통해 보는 것. 따라서 많은 제약이 따른다.

주제정하기 & 재료모으기

3D를 사용하려고 마음먹었을 때 처음 떠올린 것은 술이었다. 양주병과 양주잔, 얼음 등이 애니메이션으로 표현되면 멋질 거라고 생각했기 때문. 하지만 술병은 브랜드명이 들어가야되고 저작권 문제 때문에 3D 모델을 유료로 구매해야 하는 것들이 대부분이었다. 돈 드는건 싫으니 가볍게 포기하고 다음으로 생각한게 태양계 행성이었다.

동그란 구니까 모델이 간단할 거고,여기저기 무료로 올라와있는 소스가 많았기 때문에 적격이었다.

구글에서 3D Model Planets, Solar system glb 등의 검색어로 'Free' 딱지가 붙은 것들 위주로 찾은 다음 웹 glb 뷰어로 표시했을 때 제대로 나오는 것만 선별해서 태양계 셋트를 구성했다. 어차피 웹뷰를 통해서 보는 거라 웹에서 제대로 표시가 되야하니깐

첫 시도 : 화면에 glb 표시하기

우선 아래와 같은 _buildModelViewer라는 간단한 위젯을 만들고 무식하게 한 번 화면에 띄워봤다
웹뷰테스트를 한 번씩 거친 파일들이라 화면에 3D 모델은 잘 표시가 되었다.

하지만 몇 가지 문제들이 있었는데,

  1. PageView는 메모리 최적화를 위해 다음 페이지로 스크롤 할 때 페이지 내용을 메모리에 로드하는데 3D 모델이다보니 페이지 전환시 딜레이가 많이 생긴다.
  2. 구글이 제공하는 model-viewer는 AR 모드가 default이다보니 행성 모양이 뜨고 손가락 표시기와 함께 행성을 돌려보라는 가이드가 표시된다. 내가 원하는 것은 화면에 로드되자마자 행성이 회전을 하는 것이었다.
  3. AR 때문에 화면의 어느 지점이든 클릭해서 움직이면 행성 모델이 움직이게 된다. 즉 Swipe로 페이지 전환을 할 수가 없다.
O3DController controller = O3DController();
PageController pageController = PageController();

PageView(
  controller: pageController,
  children: <Widget>[
  _buildModelViewer('assets/3ds/Sun.glb'),
  _buildModelViewer('assets/3ds/mercury.glb'),
  _buildModelViewer('assets/3ds/venus.glb'),
  _buildModelViewer('assets/3ds/theearth.glb'),
  _buildModelViewer('assets/3ds/moon.glb'),
  _buildModelViewer('assets/3ds/mars.glb'),
  _buildModelViewer('assets/3ds/Jupiter.glb'),
  _buildModelViewer('assets/3ds/saturn.glb'),
  _buildModelViewer('assets/3ds/uranus.glb'),
  _buildModelViewer('assets/3ds/neptune.glb'),
  ],
),

Widget _buildModelViewer(String modelPath) {
  return O3D.asset(
        src: modelPath,
        controller: controller,
  );
}

2번과 3번 문제를 해결하기 위해 아래와 같이 위젯을 수정했다.

  • autoPlay와 autoRotate를 true로 주고, autoRotateDelay를 0으로 해서 화면에 뜨자마자 회전이 시작되도록 했다.
  • 회전 속도는 auto로 줬다.
  • interactionPrompt를 InteractionPrompt.none으로 해서 손가락 표시기가 뜨지 않도록 했다
  • AbsorbPointer를 줘서 행성 모델에 작용하는 클릭을 없애고 스와이프로 PageView가 작동하도록 했다.
Widget _buildModelViewer(String modelPath) {
  return AbsorbPointer(
    absorbing: true,
    child: O3D.asset(
        src: modelPath,
        controller: controller,
        autoPlay: true,
        autoRotate: true,
        autoRotateDelay: 0,
        disablePan: true,
        rotationPerSecond: 'auto',
        interactionPrompt: InteractionPrompt.none,
    ),
  );
}

1번의 페이지 전환시 딜레이가 생기는 것은 PageController에 viewportFraction을 1 미만으로 하면 다음 페이지가 미리 로드되면서 살짝 빨라지긴 한다. 비록 지금 표시중인 페이지가 약간 버벅거리지만. 하지만 다음페이지에 한정되기 때문에 여러 페이지를 스크롤하기에는 무리가 따른다. 그래서 Preload_Page_View라는 패키지를 사용했다.

preload_page_view (pub.dev)

최종 pageController는 아래와 같이 만들었다.

  O3DController controller = O3DController();
  PreloadPageController pageController = PreloadPageController(viewportFraction: 0.99);

Camera Control

화면을 아래로 스와이프 했을 때 행성모델이 아래로 내려가고 각 행성의 디테일 정보가 표시되도록 해야하는데 행성모델을 .animate로 내리는 방법도 생각해볼 수 있었지만 기왕 3D뷰어가 돌아가고 있으니 카메라 컨트롤을 통해 아래로 움직이게 해줬다.

어려웠던 점은 각 모델의 초기 cameraTarget값과 cameraOrbit값이 다 달라서 행성 모델들마다 값을 튜닝해야 한다는 거였다.

그래서 enum을 만들고 각 행성의 정보들을 enum안에 다 집어넣었다.

enum Planets {
  sun(
    model: 'assets/3ds/Sun.glb',
    name: 'Sun',
    exp:
        'The center of our Solar System, a ma-\nssive star providing light and heat',
    initPos: {'x': 0.0, 'y': 0.0, 'z': 0.26},
    initOrbit: {'theta': 0, 'phi': 75, 'radius': 1.039},
    downPos: {'x': 0.0, 'y': 0.46, 'z': 0.26},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 0.512},
    initFOV: "30deg",
    downFOV: "30deg",
    detailPage: Sun(),
  ),
  mercury(
    model: 'assets/3ds/mercury.glb',
    name: 'Mercury',
    exp:
        'Smallest planet, closest to the Sun,\nwith extreme temperature swings',
    initPos: {'x': 1.0, 'y': 0.83, 'z': 0.92},
    initOrbit: {'theta': 0, 'phi': 75, 'radius': 4.057},
    downPos: {'x': 1.0, 'y': 2.64, 'z': 0.92},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 2.00},
    initFOV: "30deg",
    downFOV: "30deg",
    detailPage: Mercury(),
  ),
  venus(
    model: 'assets/3ds/venus.glb',
    name: 'Venus',
    exp: 'Earth-like but with thick clouds\nand extreme greenhouse effect',
    initPos: {'x': 1.0, 'y': 0.83, 'z': 0.92},
    initOrbit: {'theta': 0, 'phi': 75, 'radius': 4.057},
    downPos: {'x': 1.0, 'y': 2.64, 'z': 0.92},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 2.00},
    initFOV: "30deg",
    downFOV: "30deg",
    detailPage: Venus(),
  ),

  earth(
    model: 'assets/3ds/theearth.glb',
    name: 'Earth',
    exp:
        'Only planet known to support life,\nwith blue oceans and diverse climates',
    initPos: {'x': 0.0, 'y': 0.0, 'z': 0.0},
    initOrbit: {'theta': 0, 'phi': 75, 'radius': 0.4979},
    downPos: {'x': 0.0, 'y': 0.22, 'z': 0.0},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 0.225},
    initFOV: "30deg",
    downFOV: "30deg",
    detailPage: Earth(),
  ),
  mars(
    model: 'assets/3ds/mars.glb',
    name: 'Mars',
    exp: 'The Red Planet, known for its\nhigh volcano and red surface',
    initPos: {'x': 0.0, 'y': 0.0, 'z': 0.0},
    initOrbit: {'theta': 0, 'phi': 75, 'radius': 10.14},
    downPos: {'x': 0.0, 'y': 4.5, 'z': 0.0},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 5},
    initFOV: "30deg",
    downFOV: "24deg",
    detailPage: Mars(),
  ),
  jupiter(
    model: 'assets/3ds/Jupiter.glb',
    name: 'Jupiter',
    exp: 'Largest gas giant, famous for\nits Great Red Spot and storms',
    initPos: {'x': 0.0, 'y': 0.0, 'z': 0.26},
    initOrbit: {'theta': 0, 'phi': 75, 'radius': 1.039},
    downPos: {'x': 0.0, 'y': 0.46, 'z': 0.26},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 0.512},
    initFOV: "30deg",
    downFOV: "30deg",
    detailPage: Jupiter(),
  ),
  saturn(
    model: 'assets/3ds/saturn.glb',
    name: 'Saturn',
    exp: 'Gas giant with prominent rings\nof ice and rock',
    initPos: {'x': -0.48, 'y': 0.0, 'z': 3.61},
    initOrbit: {'theta': -34, 'phi': 60, 'radius': 1896},
    downPos: {'x': -0.48, 'y': 466.4, 'z': 3.61},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 934.9},
    initFOV: "29deg",
    downFOV: "29deg",
    detailPage: Saturn(),
  ),
  uranus(
    model: 'assets/3ds/uranus.glb',
    name: 'Uranus',
    exp: 'Ice giant with a unique tilted\naxis and pale blue color',
    initPos: {'x': 0.01, 'y': 0.12, 'z': 0.26},
    initOrbit: {'theta': 0, 'phi': 151, 'radius': 40580},
    downPos: {'x': 0.01, 'y': 156563, 'z': 0.26},
    downOrbit: {'theta': -90, 'phi': 151, 'radius': 25509},
    initFOV: "30deg",
    downFOV: "30deg",
    detailPage: Uranus(),
  ),

  neptune(
    model: 'assets/3ds/neptune_flat.glb',
    name: 'Neptune',
    exp:
        'Farthest ice giant, known for its\nintense blue color and strong winds',
    initPos: {'x': 1.0, 'y': 0.83, 'z': 0.92},
    initOrbit: {'theta': 0, 'phi': 75, 'radius': 4.057},
    downPos: {'x': 1.0, 'y': 2.64, 'z': 0.92},
    downOrbit: {'theta': -90, 'phi': 90, 'radius': 2.00},
    initFOV: "30deg",
    downFOV: "30deg",
    detailPage: Neptune(),
  );

  const Planets({
    required this.model,
    required this.name,
    required this.exp,
    required this.initPos,
    required this.initOrbit,
    required this.downPos,
    required this.downOrbit,
    required this.initFOV,
    required this.downFOV,
    required this.detailPage,
  });

  final String model;
  final String name;
  final String exp;
  final Map<String, double> initPos;
  final Map<String, double> initOrbit;
  final Map<String, double> downPos;
  final Map<String, double> downOrbit;
  final String initFOV;
  final String downFOV;
  final Widget detailPage;
}

각 모델의 초기 카메라 값은 모델에디터에서 glb파일을 열어서 구할 수 있었다.
나머지는 텍스트에 적당히 이미지를 섞어서 flutter_animate 패키지를 이용해 애니메이션을 구현했다.

처음으로 도전해본 3D 애니메이션이었고 스터디 과제라서 지켜야하는 스펙이 있다보니 한계는 있었지만 o3d패키지 개발자의 예제처럼 심플한 3D 캐릭터를 이용한다든가, 일부 집중이 필요한 요소에만 3D 모델을 사용하면 (상품 홍보를 위한 AR등) 충분히 활용가치가 있어보인다.

완성한 애니메이션은 아래와 같다.
원본 이미지 링크 : imgur

profile
얼굴셋 손여섯

2개의 댓글

comment-user-thumbnail
2024년 1월 21일

데빌 님의 첫 벨로그! 영광스럽게 읽었습니다~~ 글을 이해하기 쉽게 잘 쓰시네요. IT 관련 글들은 간혹 글이 아쉬운 경우가 많은데 데빌 님은 이해하기 쉽게 글을 잘 작성해 주시네요. 덕분에 잘 읽었습니다. 저도 나중에 flutter로 3D 모델 작업 한 번 해볼래요!

1개의 답글