๐Ÿ’ฅ ๋™๊ธฐ

ํšŒ์‚ฌ์—์„œ ๋…์„œ๋น„๋ฅผ ์ง€์›ํ•ด์ฃผ๋Š” ๋•์— ์ฝ”๋กœ๋‚˜ ํ•œ์ฐธ ์ „๋ถ€ํ„ฐ ๊ฑฐ๋ฆฌ๋‘๊ธฐ๋ฅผ ํ•˜๋˜ ์ฑ…์„ ์กฐ๊ธˆ์”ฉ ์ฝ๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค. ์ตœ๊ทผ์— ์ฑ…์„ ์•Œ์•„๋ณด๋Ÿฌ ์•Œ๋ผ๋”˜์— ๋“ค์–ด๊ฐ€๋ณด๋‹ˆ ์ฑ…์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ์ฑ…์„ ๋Œ๋ ค๋ณผ ์ˆ˜ ์žˆ๋„๋ก ๋˜์–ด์žˆ์—ˆ๋‹ค.

์š”์ฆ˜ three.js์— ๋Œ€ํ•ด ํ˜ธ๊ธฐ์‹ฌ์ด ์žˆ์—ˆ๋Š”๋ฐ ์ด ๊ธฐํšŒ์— three.js ๊ฒฝํ—˜ํ•ด๋ณผ ๊ฒธ ํ˜น์‹œ ๋‹ค๋ฅธ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ์—์„œ ์จ๋จน์–ด๋ณผ ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ํ•˜๋Š” ๋งˆ์Œ์œผ๋กœ ์•Œ๋ผ๋”˜์˜ ์ฑ…์„ three.js๋กœ ํด๋ก ํ•ด๋ณด๊ฒŒ ๋˜์—ˆ๋‹ค.(์•Œ๋ผ๋”˜์—์„œ ์ฑ…์˜ ์•ž, ๋’ค, ์˜†๋ฉด์— ์‚ฌ์šฉํ•  ์ด๋ฏธ์ง€ ์†Œ์Šค๋„ ๊ตฌํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋„ˆ๋ฌด ์ข‹์•˜๋‹ค.๐Ÿ˜Š)

./3d-book-1.png

ํด๋ก ์„ ํ•˜๋ฉด์„œ ์ด๋Ÿฐ์ €๋Ÿฐ ์†Œ์Šค๋ฅผ ํ†ตํ•ด์„œ three.js๋ฅผ ์กฐ๊ธˆ์€ ์ดํ•ดํ•ด๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ ํ˜น์‹œ three.js๋ฅผ ๋ญ”๊ฐ€๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๊ณต๋ถ€ํ•ด๋ณด๊ณ  ์‹ถ์œผ์‹  ๋ถ„๋“ค์„ ์›Œํ•ด ๊ณผ์ œ ํ˜•์‹์œผ๋กœ ๋‚ด์šฉ์„ ์ •๋ฆฌ ํ•ด๋ณด์•˜๋‹ค.

๐ŸŽฎ ์•Œ๋ผ๋”˜ ์ฑ… ํด๋ก ํ•ด๋ณด๊ธฐ

๐Ÿค” ๊ณผ์ œ

์•Œ๋ผ๋”˜์˜ ์ฑ… ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ three.js๋ฅผ ํ†ตํ•ด ์ตœ๋Œ€ํ•œ ์œ ์‚ฌํ•˜๊ฒŒ ๊ตฌํ˜„ํ•ด๋ณธ๋‹ค.

์ œํ•œ์‹œ๊ฐ„: 5์‹œ๊ฐ„(๊ธด์žฅ๊ฐ์„ ์œ„ํ•ด)

๐Ÿงบ ์ค€๋น„๋ฌผ

์ฑ…์˜ ์•ž, ๋’ค, ์˜†๋ฉด์—์„œ ์‚ฌ์šฉํ•  ์ด๋ฏธ์ง€๋Š” ์—ฌ๊ธฐ์—์„œ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋ฅธ ์ฑ…์˜ ํ‘œ์ง€๋ฅผ ์–ป์–ด์˜ค๊ณ  ์‹ถ๋‹ค๋ฉด ์•Œ๋ผ๋”˜์—์„œ ์›ํ•˜๋Š” ์ฑ…์„ ๊ฒ€์ƒ‰ํ•ด์„œ ์•„๋ž˜์ฒ˜๋Ÿผ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

./3d-book-2.png

โœ… ์š”๊ตฌ์‚ฌํ•ญ

  • ์ฒ˜์Œ์—๋Š” ์ฑ…์˜ ์ •๋ฉด์ด ๋ณด์—ฌ์ง€๊ฒŒ ํ•˜๊ธฐ
  • ๋งˆ์šฐ์Šค๋ฅผ ์ฑ…์— ์˜ฌ๋ฆฌ๋ฉด ์ฑ…์˜ ์˜†๋ฉด์ด ์‚ด์ง ๋ณด์ด๋„๋ก ๋Œ๋ฆฌ๊ธฐ
  • ์•Œ๋ผ๋”˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์™€ ๋™์ผํ•œ ๊ทธ๋ฆผ์ž ๊ตฌํ˜„ํ•˜๊ธฐ
  • (์„ ํƒ) ํด๋ฆญ ์‹œ ์ฑ…์ด 180๋„ ํšŒ์ „ํ•˜๊ฒŒ ํ•˜๊ธฐ

๐Ÿ„ ๋ ˆํผ๋Ÿฐ์Šค

์•„๋ž˜๋ถ€ํ„ฐ๋Š” ๋‚ด๊ฐ€ ๋งŒ๋“  ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์„ค๋ช…์ด๋‹ค.(๐Ÿ”ฅ์Šคํฌ์ฃผ์˜!)



๐Ÿง  ๋‚ด๊ฐ€ ๋งŒ๋“  ๋ฐฉ๋ฒ•

๋‚ด๊ฐ€ ๊ตฌํ˜„ํ•ญ ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ ๋งํฌ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

๋‚ด ๋ธ”๋กœ๊ทธ๋‚˜ ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ• ์ง€๋„ ๋ชจ๋ฅด๊ธฐ์— React๋กœ ๊ฐœ๋ฐœํ•ด์„œ ๋ฐฐํฌํ•ด๋†“์•˜๋‹ค.

๐Ÿ–ฒ ๊ธฐ๋ณธ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

๋จผ์ € three.js๋กœ 3D ์„ธ์ƒ์— ์„ธํŠธ์žฅ์„ ๋งŒ๋“ค์–ด๋ณด์ž.

๐ŸŒ‰ Scene

Scene์€ ๋ง๊ทธ๋Œ€๋กœ ์žฅ๋ฉด์„ ์˜๋ฏธํ•œ๋‹ค. ๋ฌด์–ธ๊ฐ€๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ๋Š” scene์„ ๋จผ์ € ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค.

const scene = new THREE.Scene();
scene.background = new THREE.Color(style.background);

๊ทธ๋ฆฌ๊ณ  ๋‚˜์„œ ๋งŒ๋“  scene์—์„œ ๋ณด์—ฌ์ค„ ์—ฌ๋Ÿฌ ์š”์†Œ๋“ค์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

๐Ÿ“ท Camera

Camera๋Š” ๋ง๊ทธ๋Œ€๋กœ ์นด๋ฉ”๋ผ๋กœ Scene์˜ ์–ด๋””์„œ, ์–ด๋””๋ฅผ, ์–ด๋–ป๊ฒŒ ๋ณด๊ณ  ์žˆ๊ฒŒ ํ•  ๊ฒƒ์ธ๊ฐ€๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‚˜๋Š” ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ๋žŒ์˜ ๋ˆˆ์„ ๊ฐ€์žฅํ•œ ๋ฐฉ์‹์˜ ์นด๋ฉ”๋ผ์ธ PerspectiveCamera๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

const camera = new THREE.PerspectiveCamera(70, aspect, 1, 1000);
camera.position.set(0, 0, 6);

๐ŸŽฅ WebGLRenderer

Scene๊ณผ Camera๊ฐ€ ์ค€๋น„๋˜์—ˆ๋‹ค๋ฉด ์ด์ œ ๋ณด์—ฌ์ฃผ๊ธฐ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ ํ™˜๊ฒฝ์€ ์ค€๋น„๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ๋‹ค. ์ด๋ฅผ ์‹ค์ œ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ๋Š” WebGLRenderer๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

WebGLRenderer๋Š” ๋ง ๊ทธ๋Œ€๋กœ ๋‚ด๊ฐ€ ๋งŒ๋“  Scene์„ WebGL์„ ํ™œ์šฉํ•ด์„œ ๋ณด์—ฌ์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ๋‚˜๋Š” renderer๋ฅผ ๋งŒ๋“ค๋ฉด์„œ ๊ทธ๋ฆผ์ž๊ฐ€ ๋ณด์—ฌ์งˆ ์ˆ˜ ์žˆ๋„๋ก shadowMap์„ ์„ค์ •ํ•ด์ฃผ์—ˆ๋‹ค.

const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setSize(style.width, style.height);

๊ทธ๋ฆฌ๊ณ  ๋‚˜์„œ ์ด๋ ‡๊ฒŒ ์•„๋ž˜์™€ ๊ฐ™์ด animate ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด render๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด screen์— ๋ณ€ํ™”๋˜๋Š” ๋‚ด์šฉ์„ ์ง€์†์ ์œผ๋กœ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. ์—ฌ๊ธฐ์„œ setInterval์ด ์•„๋‹Œ requestAnimationFrame๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” ๋งŽ์ง€๋งŒ ํŠนํžˆ ๋ธŒ๋ผ์šฐ์ €์˜ ๋‹ค๋ฅธ ํƒญ์œผ๋กœ ์ด๋™ํ–ˆ์„ ๋•Œ๋Š” ๋ฐ˜๋ณต์ด ์ž ์‹œ ๋ฉˆ์ถฐ์„œ ์“ธ๋ฐ์—†๋Š” ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ๊ฐ€ ์—†๋‹ค๊ณ  ํ•œ๋‹ค.

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

๐Ÿ“ฆ ๋ฌผ์ฒด ์ถ”๊ฐ€ํ•˜๊ธฐ

๊ทธ๋Ÿผ ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋ฌผ์ฒด๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž. Mesh๋Š” Object์˜ ๊ธฐ๋ณธํ‹€์ด๋‹ค. Mesh์— ์—ฌ๋Ÿฌ Material์„ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ ๋ฌผ์ฒด๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ณผ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ถ”๊ฐ€ํ•ด์•ผํ•˜๋Š” Mesh๋Š” ๋ญ๊ฐ€ ์žˆ์„๊นŒ? ์–ธ๋œป ์ƒ๊ฐํ•˜๋ฉด ์ฑ…๋งŒ ์žˆ์œผ๋ฉด ๋  ๊ฒƒ ๊ฐ™์ง€๋งŒ ์ฑ…์— ์žˆ๋Š” ๊ทธ๋ฆผ์ž๊ฐ€ ๋ณด์—ฌ์ง€๊ธฐ ์œ„ํ•ด ๊ทธ๋ฆผ์ž๊ฐ€ ๋น„์ถ”์–ด์งˆ ๋ฌผ์ฒด๋„ ํ•„์š”ํ•˜๋‹ค.

๐ŸŒ˜ plane (๊ทธ๋ฆผ์ž๋ฅผ ์œ„ํ•œ ๋ฐฐ๊ฒฝ)

ํ˜น์‹œ ์ถ”๊ฐ€ ๊ตฌํ˜„์ธ ํšŒ์ „์„ ์œ„ํ•ด์„œ ๊ทธ๋ฆผ์ž๋ฅผ ๋ณด์—ฌ์ค„ ๋ฌผ์ฒด๋Š” ํŠน์ • ์ƒ‰๊น”์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๊ณ  ๊ทธ๋ƒฅ ๊ทธ๋ฆผ์ž๋งŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์ตœ๊ณ ์ผ ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ ๊ทธ๋ฆผ์ž๋งŒ์„ ๋ฐ›๊ณ  ๋‹ค๋ฅธ ๋ถ€๋ถ„๋“ค์„ ๋ชจ๋‘ ํˆฌ๋ช…ํ•œ ์ƒํƒœ์ธ ShadowMaterial์„ ํ™œ์šฉํ–ˆ๋‹ค.

const planeGeometry = new THREE.PlaneGeometry(500, 500, 32, 32);
const planeMaterial = new THREE.ShadowMaterial();
planeMaterial.opacity = 0.5;

const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.receiveShadow = true;
scene.add(plane);

๐Ÿ““ cube (์ฑ…)

์ฑ…์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ์œก๋ฉด์ฒด์ธ BoxGeometry์— ์‚ฌ์ด์ฆˆ๋ฅผ ์ •ํ•ด ์ฑ…์˜ ํ˜•ํƒœ๋ฅผ ๋งŒ๋“ค๊ณ  ์ฑ…์˜ 6๊ฐœ์˜ ๋ฉด์— ๋“ค์–ด๊ฐˆ Material๋ฅผ getBookMaterials ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

const geometry = new THREE.BoxGeometry(3.5, 5, 0.5);
const cube = new THREE.Mesh(geometry, getBookMaterials(bookCovers));
cube.castShadow = true;
cube.position.set(0, 0, 0);
scene.add(cube);

์ฑ…์˜ ์œก๋ฉด์— ๋“ค์–ด๊ฐˆ Material์„ ์ •์˜ํ•˜๋Š” ํ•จ์ˆ˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. 6๊ฐœ ๋ฉด์„ ๋Œ๋ฉด์„œ ๊ฐ ๋ฉด์— ํ•ด๋‹นํ•˜๋Š” image Url์ด ๋„˜์–ด์˜ค๋ฉด Image๋ฅผ TextureLoader๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ์–ด์„œ ๋ฉด์— ํ•ด๋‹นํ•˜๋Š” Material์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์—†๋‹ค๋ฉด ํฐ์ƒ‰ Material์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

const getBookMaterials = (urlMap) => {
  const materialNames = ['edge', 'spine', 'top', 'bottom', 'front', 'back'];
  return materialNames.map((name) => {
    if (!urlMap[name]) return new THREE.MeshBasicMaterial(0xffffff);

    const texture = new THREE.TextureLoader().load(urlMap[name]);

    // to create high quality texture
    texture.generateMipmaps = false;
    texture.minFilter = THREE.LinearFilter;
    texture.needsUpdate = true;

    return new THREE.MeshBasicMaterial({ map: texture });
  });
};

๐Ÿ’ก Light

๊ทธ๋Ÿผ ์ด์ œ ์กฐ๋ช…์„ ์„ค์น˜ํ•ด๋ณด์ž. ์กฐ๋ช…์˜ ์œ„์น˜์™€ ๊ฐ๋„๋ฅผ ์ž˜ ์กฐ์ •ํ•ด์ค˜์•ผ ์ฑ… ๋’ค์— ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ทธ๋ฆผ์ž๊ฐ€ ์ƒ๊ฒจ๋‚  ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ ํŠน์ • ์ง€์ ์—์„œ ๋น›์„ ์˜๋Š” DirectionalLight๋ฅผ ํ™œ์šฉํ•˜์˜€๋‹ค.

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(-10, 10, 10);
light.castShadow = true;
scene.add(light);

๐Ÿคธโ€โ™‚๏ธ ํšŒ์ „์‹œํ‚ค๊ธฐ

์ด์ œ ์ฑ…์— ๋งˆ์šฐ์Šค๋ฅผ ํ˜ธ๋ฒ„ํ–ˆ์„ ๋•Œ ์ฑ…์ด ์›€์ง์ด๋„๋ก ํ•ด๋ณด์ž. ๊ทผ๋ฐ ์•Œ๋ผ๋”˜์˜ ์ฑ…์ด ์›€์ง์ด๋Š” ๊ฑธ ๋ณด๋ฉด ์ฑ…์ด ์›€์ง์ž„์— ๋”ฐ๋ผ ์šฐ์ธก์— ๋ณด์ด๋˜ ๊ทธ๋ฆผ์ž๊ฐ€ ๊ฐ‘์ž๊ธฐ ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์กฐ๋ช…์ด ์ขŒ์ƒ๋‹จ์— ์žˆ๋‹ค๋ฉด ์ฑ…์„ ๋Œ๋ฆฐ๋‹ค๊ณ  ๊ทธ๋ฆผ์ž๊ฐ€ ์‚ฌ๋ผ์ง€๋ฉด ์•ˆ๋˜๋Š”๋ฐ ๋ญ”๊ฐ€ ์ด์ƒํ–ˆ๋‹ค. ๊ฐ™์€ ๊ตฌ๋„๋กœ ์‹ค์ œ๋กœ ์ฑ…๊ณผ ์กฐ๋ช…์„ ์œ„์น˜์‹œํ‚ค๊ณ  ์ฑ…์„ ๋Œ๋ฆฌ๋‹ค๊ฐ€ ์ด๊ฒŒ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‹ค ์ฑ…์„ ๋Œ๋ฆฌ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋‚ด ๋ˆˆ์˜ ์œ„์น˜๋ฅผ ๋ฐ”๊ฟ”์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ์ด๋Ÿฐ ๋ถ€๋ถ„์€ 2D ์„ธ๊ณ„์—์„œ ๊ณ ๋ฏผํ•ด๋ณด์ง€ ๋ชปํ–ˆ๋˜ ๊ฒƒ์ด๋ผ ์‹ ๊ธฐํ–ˆ๋‹คใ…Žใ…Ž

๋จผ์ € ๋งˆ์šฐ์Šค๊ฐ€ ์˜ฌ๋ผ์™€์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด eventListener๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

let isMouseOver = false;
refCurrent.addEventListener('mouseover', () => {
  isMouseOver = true;
});
refCurrent.addEventListener('mouseleave', () => {
  isMouseOver = false;
});

๊ทธ๋Ÿผ ๋งˆ์šฐ์Šค๊ฐ€ ์œ„์— ์žˆ์œผ๋ฉด ์ตœ๋Œ€ 135๋„๊นŒ์ง€ ์นด๋ฉ”๋ผ๋ฅผ ํšŒ์ „์‹œํ‚ค๊ณ  ๋งˆ์šฐ์Šค๊ฐ€ ๋– ๋‚˜๋ฉด ๋‹ค์‹œ ์›๋ž˜ ์œ„์น˜๋กœ ์„œ์„œํžˆ ๋Œ์•„์˜ค๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์นด๋ฉ”๋ผ์˜ ์œ„์น˜๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ๊ณ„์† scene์„ ๋ณด๊ณ  ์žˆ๋„๋ก lookAt ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ–ˆ๋‹ค.

const animate = () => {
  requestAnimationFrame(animate);
  isMouseOver ? rotate() : rotateBack();
  camera.lookAt(scene.position);
  renderer.render(scene, camera);
};

์นด๋ฉ”๋ผ์™€ ๋ฌผ์ฒด์˜ ๊ฑฐ๋ฆฌ๋ฅผ ์œ ์ง€ํ•œ ์ƒํƒœ๋กœ ๊ฐ๋„๋งŒ ๋ฐ”๊พธ๊ธฐ ์œ„ํ•ด์„œ ์˜ค๋žœ๋งŒ์— ์‚ผ๊ฐํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•˜์—ฌ x์™€ z ๊ฐ’์„ ๊ตฌํ–ˆ๋‹ค. ์ฑ…๊ณผ์˜ ๊ฑฐ๋ฆฌ๋Š” distanceTo ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

const distance = camera.position.distanceTo(cube.position);

const rotate = () => {
  if (degrees < 135) {
    degrees += 2;
    const radian = degrees * (Math.PI / 180);
    camera.position.x = Math.cos(radian) * distance;
    camera.position.z = Math.sin(radian) * distance;
  }
};

const rotateBack = () => {
  if (degrees > 90) {
    degrees -= 2;
    const radian = degrees * (Math.PI / 180);
    camera.position.x = Math.cos(radian) * distance;
    camera.position.z = Math.sin(radian) * distance;
  }
};

โšก๏ธ์ตœ์ข… ์ฝ”๋“œ

์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•œ 3d-book์˜ ์ตœ์ข… ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค!

import React, { useEffect, useRef } from 'react';
import * as THREE from 'three';

const Book = (props) => {
  const mountRef = useRef(null);

  useEffect(() => {
    const { bookCovers, style } = props;
    const aspect = style.width / style.height;

    const getBookMaterials = (urlMap) => {
      const materialNames = ['edge', 'spine', 'top', 'bottom', 'front', 'back'];
      return materialNames.map((name) => {
        if (!urlMap[name]) return new THREE.MeshBasicMaterial(0xffffff);

        const texture = new THREE.TextureLoader().load(urlMap[name]);

        // to create high quality texture
        texture.generateMipmaps = false;
        texture.minFilter = THREE.LinearFilter;
        texture.needsUpdate = true;

        return new THREE.MeshBasicMaterial({ map: texture });
      });
    };

    const refCurrent = mountRef.current;
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(style.background);

    const planeGeometry = new THREE.PlaneGeometry(500, 500, 32, 32);
    const planeMaterial = new THREE.ShadowMaterial();
    planeMaterial.opacity = 0.5;

    const plane = new THREE.Mesh(planeGeometry, planeMaterial);
    plane.receiveShadow = true;
    scene.add(plane);

    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(-10, 10, 10);
    light.castShadow = true;
    scene.add(light);

    const geometry = new THREE.BoxGeometry(3.5, 5, 0.5);
    const cube = new THREE.Mesh(geometry, getBookMaterials(bookCovers));
    cube.castShadow = true;
    cube.position.set(0, 0, 0);
    scene.add(cube);

    let isMouseOver = false;
    refCurrent.addEventListener('mouseover', () => {
      isMouseOver = true;
    });
    refCurrent.addEventListener('mouseleave', () => {
      isMouseOver = false;
    });

    let degrees = 90;

    const camera = new THREE.PerspectiveCamera(70, aspect, 1, 1000);
    camera.position.set(0, 0, 6);

    const renderer = new THREE.WebGLRenderer();
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setSize(style.width, style.height);

    const distance = camera.position.distanceTo(cube.position);

    const rotate = () => {
      if (degrees < 135) {
        degrees += 2;
        const radian = degrees * (Math.PI / 180);
        camera.position.x = Math.cos(radian) * distance;
        camera.position.z = Math.sin(radian) * distance;
      }
    };

    const rotateBack = () => {
      if (degrees > 90) {
        degrees -= 2;
        const radian = degrees * (Math.PI / 180);
        camera.position.x = Math.cos(radian) * distance;
        camera.position.z = Math.sin(radian) * distance;
      }
    };

    const animate = () => {
      requestAnimationFrame(animate);
      isMouseOver ? rotate() : rotateBack();
      camera.lookAt(scene.position);
      renderer.render(scene, camera);
    };

    refCurrent.appendChild(renderer.domElement);
    animate();

    return () => {
      refCurrent.removeChild(renderer.domElement);
    };
  }, [props]);

  return <div ref={mountRef} style={props.style} />;
};
export default Book;

๐Ÿคฉ ๋Š๋‚€์ 

๋ญ”๊ฐ€ ์ƒˆ๋กœ์› ๋‹ค. ์—ฌํƒœ๊นŒ์ง€์˜ ๊ฐœ๋ฐœ์€ ๋ชจ๋‹ˆํ„ฐ๋ผ๋Š” ๋‹จ๋ฉด์—์„œ ๋ณด์—ฌ์ง€๋Š” ์„ธ์ƒ์„ ๊ทธ๋ฆฌ๋Š” ๋Š๋‚Œ์ด์—ˆ๋‹ค๋ฉด ์ด๋ฒˆ ๊ฐœ๋ฐœ์€ ๋ญ”๊ฐ€ ์„ธํŠธ์žฅ์„ ๊พธ๋ฉฐ๋†“๊ณ  ๋ณด์—ฌ์งˆ ์žฅ๋ฉด์„ ์ดฌ์˜ํ•˜๋Š” ๋Š๋‚Œ์ด์—ˆ๋‹ค. ์•ˆ ์“ฐ๋˜ ์‚ผ๊ฐํ•จ์ˆ˜๋„ ์ข€ ์จ๋ณด๊ณ  ์‹ค์ œ๋กœ ์ฑ…๋„ ๋Œ๋ ค๋ณด๋ฉด์„œ ๋งŒ๋“ค์–ด๋‚ด๋‹ค๋ณด๋‹ˆ ๊ฐœ๋ฐœํ•˜๋‹ค๋ณด๋‹ˆ ์žฌ๋ฏธ๋„ ์žˆ๊ณ  ๋ณด๋žŒ๋„ ์žˆ์—ˆ๋‹ค.

์ด๊ฒŒ ์–ด๋–ป๊ฒŒ ์“ฐ์ผ์ง€ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ 3d๋กœ ๋งŒ๋“ค์–ด ๋ณผ๋งŒ ํ•œ ๊ฒƒ๋“ค์„ ์ข€ ๊ณ ๋ฏผํ•ด๋ด์•ผ๊ฒ ๋‹คใ…Žใ…Ž