앗! 3D 2D 보다 쉽다!

69

발표 자료

목록 보기
2/2
post-thumbnail

💡 2022년 6월 3일 제주 웹 컨퍼런스에서 발표했던 자료입니다.

안녕하세요, 오늘도 약을 팔러 온 지상 최강의 개발자 쥬니니 입니다.

“3D가 2D보다 어떻게 더 쉽냐?!”

라고 생각하실 수 있지만 이 약을 먹고 나면
다음에 여러분이 웹에서 3D를 다뤄야 할 일이 생겼을 때

“까짓거 한 번 해보죠 카지 형님!

하고 말하실 수 있을겁니다.

“아니 근데 님은 많이 해봤을거 아니에요?”

라고 아직도 의심이 드실 수도 있습니다.

하지만 걱.정. 마세요!
단 돈 3만 8천 6백원에!
여러분도 곧 자신감을 얻으실 수 있습니다!


Hell low, World!

이 박스를 JavaScript로 어떻게 만드는지 아십니까?
이건 마치 3D의 Hello, World! 같은거죠.

“우왕 헬로 월드니까 쉽겠네요?”

라고 생각이 드실 수 있지만
아래는 ThreeJS의 예제 코드입니다.

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 'blue' } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

function animate() {
 requestAnimationFrame( animate );

 cube.rotation.x += 0.01;
 cube.rotation.y += 0.01;

 renderer.render( scene, camera );
};

animate();

Hello, World! 가 아니라 Hell low World!
헬로 월드인데 외워야 할게 너무 많습니다.
키워드만 늘어놔도

scene, camera, renderer, geometry, material, mesh

벌써 머리가 아픕니다.
저는 이런걸로 여러분에게 “쉬워요" 라고 말하러 온게 아닙니다.

Hello, World!

제가 가져온 약을 한번 보시죠!

<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>

<a-scene>
    <a-box position="-1 0.5 -3" rotation="0 45 0" color="blue"></a-box>
</a-scene>

아~주 맛있어 보이지 않으십니까?
4줄, 그것도 script 태그를 빼면 3줄이면 박스를 만들 수 있습니다.

여러분의 환호 소리가 들리지 않으니 짤로 대체하겠습니다

A-Frame 이라는 프레임워크입니다.
단지 3D만 쉽게 다룰 수 있는게 아니라 VR 헤드셋도 쉽게 다룰 수 있게 해줍니다.

실 서비스를 만든 예시

이걸 쓰면 굉장히 쉽게 3D를 다룰 수 있게 되는데
예시를 한번 보여드리겠습니다.

이건 전남대학교에서 메타버스 캠퍼스를 구축해달라는 의뢰가 들어와
홈페이지의 메인에 넣은 3D 캠퍼스 입니다.

이걸 만들 때 A-Frame을 거의 처음 써볼 때 였는데도 하루만에 이 만큼의 비주얼이 완성되었습니다.

진짜로 쉬움(?)

“에이 거짓말 하지 마세요. 어떻게 저런게 하루만에 나와요?”

라고 생각하시겠지만
디자이너가 Blender로 다 만들어 줍니다. 어?

개발자가 할 일은 마치 디자이너에게 받은 이미지 파일을 img 태그로 넣듯이
gltf-model 태그에 디자이너에게 받은 3D 파일인 .glb 를 넣으면 됩니다.

요즘 누가 그냥 HTML 써요?

그런데 아까 보신 내용이 쌩 HTML로 되어있었잖아요?

“요즘 누가 그냥 HTML 써요 다 React 쓰는데”

그럴 줄 알고 React로도 쓸 수 있게 준비를 해왔습니다.

import 'aframe';
import { Scene, Box } from '@belivvr/aframe-react';

export default function App() {
  return (
    <Scene>
      <Box
        position={{ x: -1, y: 0.5, z: -3 }}
        rotation={{ x: 0, y: 45, z: 0 }}
        color="blue"
      />
    </Scene>
  );
}

@belivvr/aframe-react 라는 라이브러리를 사용하시면 A-Frame 에서도 사용하실 수 있습니다.
뭐가 다른가 하는 생각을 하실까봐 그것도 준비해봤습니다.

// HTML
<a-box position="-1 0.5 -3" rotation="0 45 0" color="blue"></a-box>

// React
<Box
  position={{ x: -1, y: 0.5, z: -3 }}
  rotation={{ x: 0, y: 45, z: 0 }}
  color="blue"
/>

HTML에서 쓸 때는 position, rotation 이 string으로 들어가있는데
React로 쓸 때는 Vecter 3D 형식으로 들어가있는걸 보실 수 있습니다.

React 에서도 그냥 HTML 쓰듯이 쓰면 안돼요?

그런데 또 이런 생각이 드실 수 있습니다.

“그냥 React에서도 아까 HTML에서 쓰듯이 쓰면 안돼?”

라고요.

TypeScript - React 를 쓰시면 이런 문제가 발생합니다.
기본적으로 등록 된 태그가 아니기 때문에 생기는 문제이지요.

이걸 해결하면서 더 편하게 A-Frame을 쓰기 위해서
저는 @belivvr/aframe-react 를 만들었습니다.

@belivvr/aframe-react 소개

하지만 제가 만들기 전에도 이미 aframe-react 라는 라이브러리가 존재했습니다.
무슨 차이가 있는가 하면 aframe-react 에서는 SceneEntity 태그만 사용할 수 있습니다.

Entity 태그만 있어도 다 할 수 있기는 한데,
편한 기능들 놔두고 굳이 힘들게 하나하나 하기는 너무 비효율적입니다.

마치 손가락 10개 중에 하나만 써서 개발하는 것과 비슷한 느낌이죠.
앞으로 많이 써야 할 프레임워크인데 굳이 이렇게 불편하게 쓰고싶지 않아서 만들었습니다.

다른 샘플도 보여줘!

“그럼 아까 그 박스 말고 또 뭘 할 수 있는데?”

라고 궁금해하실 분들을 위해
아바타의 머리를 간단하게 구현한걸 보여드리겠습니다.

카메라의 움직임에 따라 머리도 같은 방향을 쳐다봅니다.
이것도 굉장히 간단한 코드로 구현이 됩니다.

import 'aframe';
import 'aframe-mirror';
import { Scene, Box, Plane, Camera } from '@belivvr/aframe-react';

export default function App() {
 return (
   <Scene>
     <Plane
       position={{ x: 0, y: 0, z: -10 }}
       scale={{ x: 10, y: 10, z: 10 }}
       mirror
     />

     <Camera>
       <Box color="red" />
     </Camera>
   </Scene>
 );
}

참 쉽죠?
1인칭 게임을 만들 수 있을 것 같은 자신감이 생깁니다.

Mozilla Hubs 소개

실제로 이렇게 개발 된 서비스가 있습니다.
Mozilla에서 오픈소스로 개발중인 Hubs 라는 것이죠.
오픈소스이기 때문에 Repository 도 당연히 공개되어 있습니다.
(제가 컨트리뷰터로 참여하고 있습니다.)

웹에서 볼 수 있기 때문에 웹브라우저만 있으면
아무것도 설치 할 필요 없이 링크만 타고 들어가서 볼 수 있다는 압도적인 장점을 지니고 있습니다.

저희 회사에서 개발중인 XRCloud라는 서비스도 이걸 이용해서 만들고 있죠.
(아직 개발중인 서비스입니다 ㅠㅠ)

데모 코딩

그럼 프레임워크 소개는 여기까지 하고 데모 코딩을 하며 마무리를 하겠습니다.
뭘 만들어볼꺼냐 하면

GitHub에서 제공하는 VR 잔디를 비슷하게 따라 만들어 보는 것입니다.
이걸 똑같이 만들려면 많은 노력이 들어가기에 대충 비슷하게

이런 느낌으로 만들겁니다.
(https://github.com/juunini/jeju-web-conference-presentation)

먼저

yarn create react-app aframe-demo --template typescript

프로젝트를 만들고

yarn add aframe @belivvr/aframe-react aframe-orbit-controls aframe-environment-component

이렇게 4개의 패키지를 설치합니다.

그리고 App.tsx 파일에 아래 내용을 붙여넣어 보세요.

import 'aframe';
import 'aframe-orbit-controls';
import 'aframe-environment-component';
import { Scene, Camera } from '@belivvr/aframe-react';

export default function Grass(): JSX.Element {
  return (
    <Scene
      environment="
        flatShading: true;
        skyType: gradient;
        skyColor: #190628;
        horizonColor: #231437;
        ground: hills;
        groundYScale: 5;
        groundTexture: none;
        groundColor: #180625;
        grid: 2x2;
        gridColor: #6B95DA;
      "
      >
      <Camera
        near={1}
        wasdControlsEnabled={false}
        lookControlsEnabled={false}
        orbit-controls="
          autoRotate: true;
          autoRotateSpeed: -0.1;
          rotateSpeed: 0.1;
          target: 0 0 0;
          initialPosition: 0 8 -18;
          minPolarAngle: 0;
          maxPolarAngle: 90;
        "
      />
    </Scene>
  );
}

이런 화면이 나옵니다.
벌써 좀 있어보이지 않나요?
그런데 이대로 하기엔 너무 어두우니 카메라 아래에 Light를 추가해주세요.

<Light type="ambient" />
<Light
  type="directional"
  position={{ x: 0, y: -0.5, z: -20 }}
  intensity={3}
/>
<Light
  type="directional"
  position={{ x: 0, y: -0.5, z: 20 }}
  intensity={3}
/>

그럼 이렇게 조금 밝아집니다.
이제 시험삼아 박스를 하나 넣어보죠.

<Box color={`rgb(0, 255, 0)`} />

잘 들어갔죠?
그럼 1주일치 커밋을 한번 넣어봅시다.

const WEEK: number = 7;
const COMMITS: number[] = [
  6, 1, 3, 20, 5, 3, 7,
];

이렇게 1주일치 커밋을 아무 숫자나 7개 넣고
평소에 React에서 배열 랜더링 하듯이 map 을 이용해서 Box 를 랜더링 합니다.

높이를 커밋만큼 그대로 주면 천장을 뚫고 가버리기 땜에 1/10 만큼만 주고
포지션이 겹치면 안되니 인덱스에서 7을 나눈 나머지를 x좌표로 주겠습니다.
잔디가 커밋 수가 많으면 밝게 표시되니 그것도 반영하구요.

{
  COMMITS.map((count: number, index: number) => {
    const height: number = count / 10;

    return (
      <Box
        height={height}
        position={{ x: index % WEEK, y: 0, z: 0 }}
        color={`rgb(0, ${count}, 0)`}
      />
    );
  })
}

오~ 잘 나옵니다. 그런데 바닥에 너무 딱 붙어있어요.
이걸 떼려면 y좌표에 값을 줘야 하는데
여기서 A-Frame의 엄청난 장점이 드러납니다.

방금 추가한 map 메서드를 사용하는 코드를 Entity 태그로 감싸보겠습니다.

<Entity
  position={{ x: 0, y: 2, z: 0 }}
>
{
  COMMITS.map((count: number, index: number) => {
    const height: number = count / 10;

    return (
      <Box
        height={height}
        position={{ x: index % WEEK, y: 0, z: 0 }}
        color={`rgb(0, ${count}, 0)`}
      />
    );
  })
}
</Entity>

마치 div로 감싸듯 Entity 라는걸 사용했습니다.
바닥에서 떨어진건 좋은데 왠지 이상하게 생겼네요.
높이값을 주면 위로 길어지는게 아니라 양쪽으로 길어지기 땜에 그런거라
높이값의 절반 만큼을 위로 올려줘야 합니다.

그러니 Boxposition yheight / 2 만큼을 할당해주어야 하죠

position={{ x: index % WEEK, y: height / 2, z: 0 }}

이제 높이가 잘 맞네요.
1주일치를 넣어봤으니 그 이상도 넣어봐야 합니다.
샘플로 4주일치를 넣어볼게요.

const COMMITS: number[] = [
  6, 1, 3, 20, 5, 3, 7,
  2, 4, 1, 1, 2, 5, 18,
  8, 3, 1, 1, 9, 21, 3,
  1, 1, 15, 5, 49, 44, 68,
];

// ...

<Entity
  position={{ x: 0, y: 2, z: 0 }}
>
  {
    COMMITS.map((count: number, index: number) => {
      const height: number = count / 10;
      // ↓
      const currentWeek: number = Math.floor(index / WEEK);
      // ↑

      return (
        <Box
          height={height}
          // ↓ z
          position={{ x: index % WEEK, y: height / 2, z: currentWeek }}
          // ↑ z
          color={`rgb(0, ${count}, 0)`}
        />
      );
    })
  }
</Entity>

currentWeek 라는 값이 생겼고 그걸 Boxposition z 에 할당했습니다.
x축은 할당이 되어서 겹치지 않는데 z축이 할당되지 않아 겹치기 때문에
해당 주차 만큼 z축을 이동시키는 것이죠.

그럼 이렇게 나옵니다.

4주차도 되었으니 GitHub의 VR 잔디처럼 1년치를 넣어보죠.

function generateRandomNumber(min: number, max: number) {
  return Math.round(Math.random() * (max - min) + min);
}

const WEEK: number = 7;
const NUMBER_OF_WEEKS: number = 34;
const COMMITS: number[] = [...new Array(WEEK * NUMBER_OF_WEEKS)]
  .map(() => generateRandomNumber(0, 30));

아예 0~30 까지의 랜덤한 숫자를 7 * 34 만큼 만들어서 배열에 할당했습니다.

잘 만들어지긴 했는데 중앙 정렬이 안되고 처음 로딩 했을 때 세로로 길쭉합니다.
가로로 돌려야 하기도 하고 중앙 정렬도 해야겠죠?

이것도 div 처럼 감싸고 있는 Entity 에 값을 할당하면 일괄적으로 적용이 됩니다.

<Entity
  position={{ x: -NUMBER_OF_WEEKS / 2, y: 2, z: WEEK / 2 }}
  rotation={{ x: 0, y: 90, z: 0 }}
>

이렇게 완성이 되는 것이죠.

https://github.com/juunini/jeju-web-conference-presentation
여기서 코드를 전부 보실 수 있는데, 이 코드가 고작 80줄밖에 되지 않습니다.

여러분도 한번 해보세요.
포트폴리오에도 가볍게 넣어보기 좋고
실무에서 써야 할 때에도 빠르게 적용해볼 수 있습니다.

이제 내 라이브러리에 구독과 좋아요 알림설정이 많이 늘겠지?

profile
지상 최강의 개발자 쥬니니

10개의 댓글

comment-user-thumbnail
2022년 6월 5일

재미있는 내용입니다. 잘봤습니다. ^^/

1개의 답글
comment-user-thumbnail
2022년 6월 10일

3D는 정말 할 생각 못해봤는데, 습작 형태라도 가볍게 해볼 수 있겠네요. 감사합니다!

1개의 답글
comment-user-thumbnail
2022년 6월 13일

너무 재밌어 보여서 오늘 해보려고요~~!

1개의 답글

꿀잼

1개의 답글
comment-user-thumbnail
2022년 7월 4일

선생님 정말 개쩌는분이신거 같아요 존경합니다
혹시 회사에 웹개발자로 가려면 스택 nodeJS 말고 딴거 쓰시는거 잇을까얌?

1개의 답글