춘식이 관찰일기 사이트처럼 캐릭터를 움직여본다.

230613-1.gif

위와 같이 원하는 위치로 이동 시 이벤트가 발생하도록 구현한다. 구현한 기능을 설명하는 것 위주로 작업해봄.

ilbunidiary/src/main.js

import * as THREE from "three";

// Texture
const textureLoader = new THREE.TextureLoader();
const floorTexture = textureLoader.load("/images/grid.png");
floorTexture.wrapS = THREE.RepeatWrapping;
floorTexture.wrapT = THREE.RepeatWrapping;
floorTexture.repeat.x = 10;
floorTexture.repeat.y = 10;

먼저 텍스쳐 설정을 해본다. 밑 바닥 grid를 설정한 것임. repeat 메서드를 사용해서 반복 정도를 조절할 수 있음

// ..

// Renderer
const canvas = document.querySelector("#three-canvas");
const renderer = new THREE.WebGLRenderer({
  canvas,
  antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio > 1 ? 2 : 1);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Renderer에서는 기본 Renderer 설정 후 shadowMap.type을 PCFSoftShadowMap을 사용해 부드러운 그림자를 표현하도록 설정해주었음

// ..
// Scene
const scene = new THREE.Scene();

// Camera - 직교 카메라 사용, 객체가 어디있던 동일한 크기로 보여준다. (2D와 비슷함)
const camera = new THREE.OrthographicCamera(
  -(window.innerWidth / window.innerHeight), // left
  window.innerWidth / window.innerHeight, // right,
  1, // top
  -1, // bottom
  -1000, // near
  1000 // far
);

const cameraPosition = new THREE.Vector3(1, 5, 5);
camera.position.set(cameraPosition.x, cameraPosition.y, cameraPosition.z);
camera.zoom = 0.2;
camera.updateProjectionMatrix();
scene.add(camera);

// Light
const ambientLight = new THREE.AmbientLight('white', 0.7);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight('white', 0.5);
const directionalLightOriginPosition = new THREE.Vector3(1, 1, 1);
directionalLight.position.x = directionalLightOriginPosition.x;
directionalLight.position.y = directionalLightOriginPosition.y;
directionalLight.position.z = directionalLightOriginPosition.z;
directionalLight.castShadow = true;

// mapSize 세팅으로 그림자 퀄리티 설정
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
// 그림자 범위
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = -100;
directionalLight.shadow.camera.far = 100;
scene.add(directionalLight);

기본 Scene 추가 후 Camera는 OrthographicCamera를 사용함. 이는 직교 카메라로 객체가 어디있던 동일한 크기로 보여주는 특징을 가진다. 마우스 컨트롤에 따라 확대되지 않으므로 2D와 비슷하게 보여지게 해줌. 각 인자로 left, right, top, bottom, near, far 등의 정보를 추가해주었다.

다음은 Mesh Mesh는 floor와 pointer, spot Mesh로 구성된다. 셋 다 Plan Mesh임

// Mesh
const meshes = [];
const floorMesh = new THREE.Mesh(
  new THREE.PlaneGeometry(100, 100),
  new THREE.MeshStandardMaterial({
    map: floorTexture
  })
);
floorMesh.name = 'floor';
floorMesh.rotation.x = -Math.PI / 2; // 바닥이어야 하므로 -90도
floorMesh.receiveShadow = true;
scene.add(floorMesh);
meshes.push(floorMesh);

const pointerMesh = new THREE.Mesh(
  new THREE.PlaneGeometry(1, 1),
  new THREE.MeshBasicMaterial({
    color: 'crimson',
    transparent: true,
    opacity: 0.5
  })
);
pointerMesh.rotation.x = -Math.PI / 2; // 바닥이어야 하므로 -90도
pointerMesh.position.y = 0.01;
pointerMesh.receiveShadow = true;
scene.add(pointerMesh);

const spotMesh = new THREE.Mesh(
  new THREE.PlaneGeometry(3, 3),
  new THREE.MeshStandardMaterial({
    color: 'yellow',
    transparent: true,
    opacity: 0.5
  })
);
spotMesh.position.set(5, 0.005, 5);
spotMesh.rotation.x = -Math.PI / 2; // 바닥이어야 하므로 -90도
spotMesh.receiveShadow = true;
scene.add(spotMesh);

다음으로는 캐릭터와 집은 gltf 로더로 로드해온 파일이다.

const gltfLoader = new GLTFLoader();

const house = new House({
  gltfLoader,
  scene,
  modelSrc: '/models/house.glb',
  x: 5,
  y: -1.3,
  z: 2
});

const player = new Player({
  scene,
  meshes,
  gltfLoader,
  modelSrc: '/models/ilbuni.glb'
});

House 클래스는 아래와 같이 구현한다.

src/House.js

export class House {
	constructor(info) {
		this.x = info.x;
		this.y = info.y;
		this.z = info.z;

		this.visible = false; // 처음엔 안보이게

		info.gltfLoader.load(
			info.modelSrc,
			glb => {
				this.modelMesh = glb.scene.children[0];
				this.modelMesh.castShadow = true;
				this.modelMesh.position.set(this.x, this.y, this.z);
				info.scene.add(this.modelMesh);
			}
		);
	}
}

Player 클래스도 아래와 같이 구현함

import {
	AnimationMixer
} from 'three';

export class Player {
	constructor(info) {
		this.moving = false; // 걸어가는 상태 체크를 위해 생성

		info.gltfLoader.load(
			info.modelSrc,
			glb => {
				// 그림자 표현 traverse로 구현
				glb.scene.traverse(child => {
					if (child.isMesh) {
						child.castShadow = true;
					}
				});
		
				this.modelMesh = glb.scene.children[0];
				this.modelMesh.position.y = 0.3;
				this.modelMesh.name = 'ilbuni';
				info.scene.add(this.modelMesh);
				info.meshes.push(this.modelMesh);

				this.actions = [];
		
				this.mixer = new AnimationMixer(this.modelMesh);
				this.actions[0] = this.mixer.clipAction(glb.animations[0]); // 일반 애니메이션
				this.actions[1] = this.mixer.clipAction(glb.animations[1]); // 걷기 애니메이션
				this.actions[0].play(); // 까딱까닥 움직임은 기본적으로 적용
			}
		);
	}
}

다음으로 raycaster 코드를 추가한다. raycaster는 마우스 클릭 시 해당 좌표로 캐릭터가 이동시켜야하므로 필요함