κΉƒν—™ μ½”λ“œ : https://github.com/soheee-bae/React-Three-Fiber/tree/main/basic

OrbitControls (ꢀ도 μ»¨λ“œλ‘€)

OrbitControlsλ₯Ό μΆ”κ°€ν•˜κ²Œ 되면 카메라λ₯Ό νšŒμ „μ‹œμΌœ μ—¬λŸ¬ μ‹œκ°μœΌλ‘œ μž₯면을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. OrbitControlsλŠ” Three.js 클래슀의 일뢀가 μ•„λ‹ˆκΈ°μ— 직접 jsx νŒŒμΌμ—μ„œ μ‚¬μš©ν•  수 μžˆλ„λ‘ React Three Fiberμ—μ„œ μ œκ³΅ν•˜λŠ” extendλ₯Ό μ‚¬μš©ν•΄μ„œ λ³€ν™˜μ‹œμΌœμ€˜μ•Ό ν•©λ‹ˆλ‹€.

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { extend, useFrame } from '@react-three/fiber'

extend({ OrbitControls })

OrbitControlsλ₯Ό μ‚¬μš©ν•  λ•Œ ν•„μˆ˜μΈ 두 개의 λ§€κ°œλ³€μˆ˜κ°€ μžˆλŠ”λ°μš”. μ΄λŠ” μœ„μ˜ κΈ€μ—μ„œ λ΄€μ—ˆλ˜ useFrame의 stateκ³Ό 같은 값을 μ£ΌλŠ” useThreeμ—μ„œ 가져와 μ§€μ •ν•΄ 쀄 수 μžˆμŠ΅λ‹ˆλ‹€.

useThreeState

useThree()의 κ°’
import { useThree, extend, useFrame } from '@react-three/fiber'

export default function Experience(){
    const { camera, gl } = useThree()
    return
        <>
            <orbitControls args={ [ camera, gl.domElement ] } />
            {/* ... */}
        </>
}

μœ„μ˜ μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜λ©΄ λ°‘κ³Ό 같은 κΈ°λŠ₯이 μ μš©λ©λ‹ˆλ‹€.


Light (λΉ›)

3Dκ°œλ°œμ—μ„œ ν˜„μ‹€κ°μ„ μ£ΌκΈ° μœ„ν•΄μ„œ 빛은 μž₯λ©΄μ—μ„œ κΌ­ ν•„μš”ν•œ μš”μ†Œ μ€‘μ˜ ν•˜λ‚˜μž…λ‹ˆλ‹€. μ—¬κΈ°μ„œ μ€‘μš”ν•œ 건 κ°œμ²΄κ°€ 빛에 λ°˜μ‘μ„ ν•˜λ €λ©΄ <meshBasicMaterial> 재질이 μ•„λ‹Œ <meshStandardMaterial> μž¬μ§ˆμ„ 써야 ν•œλ‹€λŠ” μ μž…λ‹ˆλ‹€.

빛에도 μ—¬λŸ¬ μ’…λ₯˜κ°€ μžˆλŠ”λ°μš”. λŒ€ν‘œμ μœΌλ‘œ <directionalLight>κ³Ό <ambientLight>κ°€ 있으며 λΉ›μ˜ μœ„μΉ˜λ‚˜ 강도 등을 μ‘°μ ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€.

export default function Experience(){
    // ...
    return
        <>
            <directionalLight position={ [ 1, 2, 3 ] } intensity={ 1.5 } />
            <ambientLight intensity={ 0.5 } />
            {/* ... */}
        </>
}

λ‚΄ λ§˜λŒ€λ‘œ ν˜•μƒ λ§Œλ“€κΈ°

μ§€κΈˆκΉŒμ§€ Three.js와 React Three Fiber이 μ œκ³΅ν•˜λŠ” geometry(ν˜•μƒ)을 μ‚¬μš©ν–ˆμ—ˆλŠ”λ°μš”. λ¬Όλ‘  μ œκ³΅λ˜λŠ” ν˜•μƒλ“€μ„ μ‚¬μš©ν•  μˆ˜λ„ μžˆμ§€λ§Œ 직접 μ›ν•˜λŠ” ν˜•μƒμ„ λ§Œλ“€μ–΄μ„œ μ‚¬μš©ν•  수 도 μžˆμŠ΅λ‹ˆλ‹€.


쀀비단계

λͺ¨λ“  ν˜•μƒμ€ 3개의 κΌ­μ§“μ μœΌλ‘œ λ§Œλ“€μ–΄μ§„ μ‚Όκ°ν˜•λ“€μ΄ λͺ¨μ—¬ 이루어져 μžˆλŠ”λ°μš”. μš°μ„  μ›ν•˜λŠ” ν˜•μƒμ„ λ§Œλ“€κΈ° μœ„ν•΄μ„œλŠ” κΌ­μ§“μ μ˜ 수λ₯Ό 계산해 μ£Όκ³  Float32Arrayλ₯Ό μ΄μš©ν•΄μ„œ 각 κΌ­μ§“μ λ“€μ˜ μœ„μΉ˜λ₯Ό array ν˜•νƒœλ‘œ μ €μž₯ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.

export default function CustomObject()
{
  const verticesCount = 10 * 3 // 10개의 μ‚Όκ°ν˜•κ³Ό 각각 3개의 꼭짓점
  const positions = useMemo(() => {
    const positions = new Float32Array(verticesCount * 3)
    // verticesCount에 3을 κ³±ν•˜λŠ” μ΄μœ λŠ” ν•˜λ‚˜μ˜ 점당 3개의 값이 ν•„μš”ν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€ (x,y,z)

    //for loop을 μ΄μš©ν•΄ 랜덀 값을 array에 λ„£μ–΄μ€λ‹ˆλ‹€.
    for(let i = 0; i < verticesCount * 3; i++)
        positions[i] = (Math.random() - 0.5) * 3
        return positions
    })
    // ...
}

μœ„μΉ˜ 속성을 μ΄μš©ν•΄μ„œ ν˜•μƒ λ§Œλ“€κΈ° (+BufferGeometryκ³Ό BufferAttribute)

μœ„μ—μ„œ λ§Œλ“€μ—ˆλ˜ κΌ­μ§“μ λ“€μ˜ μœ„μΉ˜λ“€μ„ κ°€μ§€κ³  μžˆλŠ” arrayλ₯Ό BufferAttributeλ₯Ό 톡해 BufferGeometry에 속성을 ν¬ν•¨μ‹œν‚€λ©΄ κ·Έ 속성을 κ°€μ§„ ν•˜λ‚˜μ˜ ν˜•μƒμ„ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.

μ—¬κΈ°μ„œ 제일 μ€‘μš”ν•œ 건 BufferAttribute에 μ–΄λ–€ 속성을 ν¬ν•¨μ‹œν‚€κ³  싢은지λ₯Ό μ•Œλ €μ€˜μ•Ό ν•˜λŠ”λ°μš”. μ΄λŠ” attachλ₯Ό μ‚¬μš©ν•΄μ„œ μ•Œλ €μ€„ 수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ’‘ μœ„μΉ˜ 속성은 λ§Žμ€ 속성 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€. 이 외에도 color, normal, uv, uv2 λ“±κ³Ό 같은 속성듀이 μžˆμŠ΅λ‹ˆλ‹€.

<bufferGeometry>
    <bufferAttribute
        attach="attributes-position" // geometry.attribute.positionκ³Ό κ°™μŠ΅λ‹ˆλ‹€
        count={ verticesCount } //꼭짓점 갯수
        itemSize={ 3 } // arrayμ—μ„œ ν•˜λ‚˜μ˜ 꼭짓점을 κ΅¬μ„±ν•˜λŠ” ν•­λͺ©μ˜ 수
        array={ positions } // κΌ­μ§“μ λ“€μ˜ μœ„μΉ˜λ“€μ„ κ°€μ§€κ³  μžˆλŠ” array
    />
</bufferGeometry>

μœ„μ˜ μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜λ©΄ λ°‘κ³Ό 같은 ν˜•μƒμ΄ λ§Œλ“€μ–΄μ§‘λ‹ˆλ‹€.

bufferGeometry


Double Side μΈ‘λ©΄ λ Œλ”λ§ ν•˜κΈ°

κΈ°λ³Έκ°’μœΌλ‘œ μœ„μ˜ ν˜•μƒμ„ λ§Œλ“€μ—ˆμ„ λ•Œ μ•žλ©΄λ§Œ λ Œλ”λ§μ΄ λ©λ‹ˆλ‹€. λ‚˜μ€‘μ— orbitControlλ₯Ό μ‚¬μš©ν•΄ ν˜•μƒμ˜ λͺ¨λ“  면을 λ‹€ λ³Ό λ•Œλ₯Ό λŒ€λΉ„ν•΄μ„œ Double side (μ•žλ’€ λ‘˜ λ‹€) 츑면을 λ Œλ”λ§ ν•˜λŠ” 것을 μΆ”μ²œν•©λ‹ˆλ‹€.

import * as THREE from 'three' λ˜λŠ” import { DoubleSide } from 'three'
<meshBasicMaterial color="red" side={ THREE.DoubleSide } />

computeVertexNormals을 μ΄μš©ν•΄μ„œ normal 속성 ν¬ν•¨μ‹œν‚€κΈ°

λ§Œλ“  ν˜•μƒμ΄ 빛에 λ°˜μ‘ν•˜κ²Œ ν•˜κΈ° μœ„ν•΄μ„œλŠ” <meshBasicMaterial> 재질이 μ•„λ‹Œ <meshStandardMaterial> μž¬μ§ˆμ„ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

<mesh>
    <meshStandardMaterial color="red" side={ THREE.DoubleSide } />
</mesh>

μœ„μ˜ μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜λ©΄ λ°‘κ³Ό 같은 화면이 λ‚˜μ˜€λŠ”λ°μš”. λ§Œλ“  ν˜•μƒμ— 색을 μ œλŒ€λ‘œ μž…νžˆκΈ° μœ„ν•΄μ„œ normal 속성을 ν¬ν•¨μ‹œμΌœμ€˜μ•Ό ν•©λ‹ˆλ‹€. μ‰¬μš΄ λ°©λ²•μœΌλ‘œ computeVertexNormalsλ₯Ό μ‚¬μš©ν•΄ ν¬ν•¨μ‹œν‚€λŠ” 방법이 μžˆμŠ΅λ‹ˆλ‹€.

withoutNormal

import { useRef, useMemo } from 'react'

export default function CustomObject(){
    const geometryRef = useRef()
    const positions = useMemo(() => {
        // ...
    })

    // positionsκ°€ λ Œλ”λ§ λ λ•Œλ§ˆλ‹€ λΆˆλŸ¬μ˜΅λ‹ˆλ‹€.
    useEffect(() => {
        geometryRef.current.computeVertexNormals()
    }, [ positions ])
    // ...
    <bufferGeometry ref={ geometryRef }>
        {/* ... */}
    </bufferGeometry>
}

μœ„μ˜ μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜λ©΄ λ°‘κ³Ό 같은 ν™”λ©΄μ²˜λŸΌ λ§Œλ“  ν˜•μƒμ— 빛에 λ°˜μ‘ν•˜λŠ” 재질이 μ μš©λ©λ‹ˆλ‹€.


Camera μ„€μ •ν•˜κΈ°

κΈ°λ³Έ Camera

μΊ”λ²„μŠ€μ— κΈ°λ³Έκ°’μœΌλ‘œ PerspectiveCameraλ₯Ό μ„€μ •ν•΄ 쀄 수 μžˆμŠ΅λ‹ˆλ‹€. PerspectiveCameraλŠ” μ‚¬λžŒμ˜ 눈으둜 λ³΄λŠ” 방식을 λͺ¨λ°©ν•˜μ—¬ μ„€κ³„λ˜μ—ˆμœΌλ©° 3D μž₯면을 λ Œλ”λ§ ν•˜λŠ”λ° κ°€μž₯ 많이 μ‚¬μš©λ©λ‹ˆλ‹€. fov, near, far, position λ“± 더 μžμ„Ένžˆ 값을 μ§€μ •ν•΄ μ£Όλ©° μΉ΄λ©”λΌμ˜ μœ„μΉ˜λ₯Ό λ³€κ²½ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

<Canvas camera={ { fov: 45, near: 0.1, far: 200 } }>
    <Experience />
</Canvas>

OrthographicCamera μ‚¬μš©ν•˜κΈ°

OrthographicCameraλŠ” λ Œλ”λ§ 된 μ΄λ―Έμ§€μ—μ„œ 객체의 ν¬κΈ°λŠ” μΉ΄λ©”λΌμ™€μ˜ 거리에 관계없이 μΌμ •ν•˜κ²Œ μœ μ§€λ©λ‹ˆλ‹€.

<Canvas camera={ { fov: 45, near: 0.1, far: 200 } }>
    <Experience />
</Canvas>

Camera에 μ• λ‹ˆλ©”μ΄μ…˜ κΈ°λŠ₯ μΆ”κ°€ ν•˜κΈ°

OrbitControlsκ³Ό λΉ„μŠ·ν•œ 효과λ₯Ό λ‚΄κ³  μ‹Άμ§€λ§Œ μ‚¬μš©μžκ°€ 화면을 μ‘°μ •ν•˜μ§€ λͺ»ν•˜κ²Œ ν•˜κ³  싢을 λ•Œ 이 방법이 많이 μ‚¬μš©λ˜λŠ”λ°μš”. 이전 κΈ€μ—μ„œ mesh에 μ• λ‹ˆλ©”μ΄μ…˜μ„ μ£ΌκΈ° μœ„ν•΄ 썼던 방법과 λΉ„μŠ·ν•˜μ§€λ§Œ μ΄λ²ˆμ—” meshκ°€ μ•„λ‹Œ 카메라에 μ• λ‹ˆλ©”μ΄μ…˜ κΈ°λŠ₯을 μΆ”κ°€ν•΄ 쀄 κ²ƒμž…λ‹ˆλ‹€.

κ°„λ‹¨νžˆ 카메라에 νšŒμ „ μ• λ‹ˆλ©”μ΄μ…˜μ„ μ£ΌκΈ° μœ„ν•΄μ„œλŠ” λ¨Όμ € 각도λ₯Ό μ•Œμ•„λ‚΄μ•Ό ν•˜κ³  각도와 sin() 및 cos()λ₯Ό μ΄μš©ν•΄μ„œ x 및 z μ’Œν‘œλ₯Ό μ–»μ–΄ μΉ΄λ©”λΌμ˜ μœ„μΉ˜λ₯Ό μ§€μ •ν•΄ μ€˜μ•Ό ν•©λ‹ˆλ‹€. μ—¬κΈ°μ„œ κ°λ„λŠ” useFrame이 μ œκ³΅ν•˜λŠ” state의 clock.elapsedTime이 μ‚¬μš©λ©λ‹ˆλ‹€.

πŸ’‘ stateμ—λŠ” 카메라, λ Œλ”λŸ¬, μž₯λ©΄ λ“±κ³Ό 같은 three.jsν™˜κ²½μ— λŒ€ν•œ 정보가 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

useFrame((state, delta) => {
    const angle = state.clock.elapsedTime
    state.camera.position.x = Math.sin(angle) * 8
    state.camera.position.z = Math.cos(angle) * 8
    state.camera.lookAt(0, 0, 0)
    // ...
})

μœ„μ˜ μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜λ©΄ λ°‘κ³Ό 같은 μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ μš©λ©λ‹ˆλ‹€.