การใช้ไลบรารี่ฟิสิกส์ (Adding Physics)

การใช้ไลบรารี่ฟิสิกส์ three.js

แอนิเมชั่นสามารถทำได้โดยใช้ไลบรารี่ฟิสิกส์ (Physics library) เราสามารถใช้ไลบรารี่ที่เรียกว่า Cannon.js แต่แทนที่จะใช้ Cannon.js ดั้งเดิมซึ่งไม่ได้รับการดูแลอีกต่อไป เราสามารถเลือกใช้ Cannon.js ใหม่ ที่ชื่อ use-cannon แทนได้

ไลบรารี่ฟิสิกส์ Cannon เหมาะอย่างยิ่งสำหรับการจำลองวัตถุที่มีความแข็ง เราจะใช้มันเพื่อทำให้วัตถุเคลื่อนที่และโต้ตอบในลักษณะที่สมจริงยิ่งขึ้น และให้โอกาสในการตรวจจับการชน

ข้อกำหนดเบื้องต้น


ข้อกำหนดสำหรับบทความนี้คือ คุณควรได้ทำตามบทความ ควบคุมการลาก (Implementing Drag Controls) มาก่อน

การใช้ไลบรารี่ฟิสิกส์


ในบทความที่แล้ว เราได้เพิ่มการควบคุมการลาก และในบทความนี้ เราจะพูดถึงฟิสิกส์ก่อนที่เราจะทำอย่างนั้น สิ่งหนึ่งที่ฉันสังเกตเห็นคือเมื่อใดก็ตามที่มีการขยายขนาดหน้าจอดูเหมือนว่าพื้นหลังของฉากจะถูกคำนวณใหม่ นั่นจะทำให้การพัฒนาช้าลงเล็กน้อย และจะหาวิธีแก้ไข

useMemo

นั่นคือการใช้ memo จาก react useMemo ซึ่งเป็นอีกหนึ่งฟังก์ชัน Hook ของ React ซึ่งเราจะเรียกใช้งาน useMemo นี้ได้ภายใน React Component เท่านั้น และวิธีการใช้งานเพื่อ ไม่ให้เกิดการกำหนดค่าหรือการเรียกใช้งานซ้ำซ้อน หากมีการ rerender ตัว component นั้นในรอบใหม่ ถือว่าเป็นการใช้งานเพื่อเพิ่มประสิทธิภาพของโปรแกรมได้เลยทีเดียว

โดยไปที่คอมโพเนนท์พื้นหลัง Background.jsx แล้วนำเข้า useMemo from react

import { useMemo } from 'react';

โค้ด Background.jsx

import { useLoader, useThree } from 'react-three-fiber';
import * as THREE from 'three';
import { useMemo } from 'react';

const Background = props => {
  const texture = useLoader(
    THREE.TextureLoader,
    '/autoshop.jpg'
  );

  const { gl } = useThree();
  const formatted = useMemo(() =>
    new THREE.WebGLCubeRenderTarget(
      texture.image.height
    ).fromEquirectangularTexture(gl, texture)
    , [])

  return (
    <primitive
      attach='background'
      object={formatted}
    />
  )
}

export default Background;


ทดสอบ ย่อ-ขยายขนาดหน้าจอดู จะเห็นประสิทธิภาพที่ดีขึ้น

use-cannon

สิ่งแรกที่เราจะทำคือเราจะนำเข้าฟิสิกส์จากการใช้แคนนอน ที่ไฟล์ App.js

import { Physics } from 'use-cannon';

สิ่งต่อไปที่เราต้องทำคือห่อวัตถุใดก็ตามที่เราต้องการให้ได้รับผลกระทบจากฟิสิกส์ด้วยแท็กฟิสิกส์ <Physics> </Physics>

        <Physics>
          <Draggable>
            <Suspense fallback={null}>
              <Box position={[-4, 1, 0]} />
            </Suspense>
            <Suspense fallback={null}>
              <Box position={[4, 1, 0]} />
            </Suspense>
          </Draggable>
          <Suspense fallback={null}>
            <Background />
          </Suspense>
          <Floor position={[0, -0.5, 0]} />
        </Physics>

เอาล่ะ เรามีส่วนประกอบ Draggable กับกล่อง 2 กล่อง พื้นและหลอดไฟ เราจะเข้าไปในนั้น และจะไม่เพิ่มคุณสมบัติใดๆ ลงไป แต่ก็ยังต้องการให้มันทำงานได้ ก็เลยรู้ว่าไม่มีอะไรเปลี่ยนแปลงจริงๆ ฟิสิกส์จะไม่ทำอะไรเลย เว้นแต่ว่าเราจะเพิ่มคุณสมบัติทางฟิสิกส์ให้กับการวัดแต่ละอย่างในฉากของเรา

โค้ดไฟล์ App.js

import './App.css';
import {
  Canvas, useFrame
} from 'react-three-fiber';
import { Physics } from 'use-cannon';
import { Suspense } from 'react';
import Orbit from './components/Orbit';
import Box from './components/Box';
import Background from './components/Background';
import Floor from './components/Floor';
import Bulb from './components/Bulb';
import Draggable from './components/Draggable';
import ColorPicker from './components/ColorPicker';

function App() {

  return (
    <div style={{ height: '100vh', width: '100vw' }}>
      < ColorPicker />
      <Canvas
        shadowMap
        style={{ background: 'black' }}
        camera={{ position: [7, 7, 7] }}
      >
        <ambientLight intensity={0.2} />
        <Bulb position={[0, 3, 0]} />
        <Orbit />
        <axesHelper args={[5]} />
        <Physics>
          <Draggable>
            <Suspense fallback={null}>
              <Box position={[-4, 1, 0]} />
            </Suspense>
            <Suspense fallback={null}>
              <Box position={[4, 1, 0]} />
            </Suspense>
          </Draggable>
          <Suspense fallback={null}>
            <Background />
          </Suspense>
          <Floor position={[0, -0.5, 0]} />
        </Physics>
      </Canvas>
    </div>
  );
}

export default App;


เริ่มจากพื้น Floor.jsx กันก่อน ดังนั้นหากต้องการเพิ่มคุณสมบัติทางกายภาพให้กับ mesh เราต้องการค้นหารูปร่างที่ดีที่ใกล้เคียงกับ mesh แต่ในกรณีนี้ พื้นของเราเป็นแบบกล่อง เราจะใช้อย่างแรกคือ นำเข้า useBox จาก use-cannon

import { useBox } from 'use-cannon';


จากนั้นในส่วนประกอบพื้นของเรา เราจะเรียกใช้ useBox แล้ว extract จาก ref, api

const [ref, api] = useBox()


โดยพื้นฐานแล้ว เราต้องใส่การอ้างอิงนี้กับวัตถุใดก็ตามที่เราต้องการใช้รูปร่างและคุณสมบัตินี้ ดังนั้นเราจะเพิ่มผู้อ้างอิงนี้ไปยัง mesh ของเรา

<mesh ref={ref} {...props} receiveShadow>


จึงจำเป็นต้อง callback

const [ref, api] = useBox(() => ({...props}))

ไฟล์ Box.jsx เราจะนำเข้ากล่อง แล้วสร้างกล่อง โดย นำเข้า useBox จาก use-cannon

import { useBox } from 'use-cannon';


และเนื่องจากเราจะได้ ref ใหม่จากสิ่งนี้ ฉันจะลบผู้อ้างอิงเก่าที่ มีที่นี่แล้วส่งอุปกรณ์ประกอบฉาก

const [ref, api] = useBox(() => ({mass: 1, ...props}))

ที่ไฟล์ Floor.jsx เพิ่ม args: [20,1,10],

โค้ดไฟล์ Floor.jsx

import { useBox } from 'use-cannon';

const Floor = props => {
  const [ref, api] = useBox(() => ({args: [20,1,10], ...props}))
    return (
      <mesh ref={ref} {...props} receiveShadow>
        <boxBufferGeometry args={[20, 1, 10]} />
        <meshPhysicalMaterial />
      </mesh>
    )
  }

  export default Floor;


ไฟล์ Box.jsx ลบ useFrame ออก

เพิ่ม api={api}

โค้ดไฟล์ Box.jsx

import { useRef } from 'react';
import { useLoader, useFrame } from 'react-three-fiber';
import * as THREE from 'three';
import { useBox } from 'use-cannon';

const Box = props => {
    const [ref, api] = useBox(() => ({mass: 1, ...props}))
    const texture = useLoader(
      THREE.TextureLoader,
      '/wood.jpg'
    );
   
    const handlePointerDown = e => {
      console.log(e)
      e.object.active = true;
      if (window.activeMesh) {
        scaleDown(window.activeMesh)
        window.activeMesh.active = false;
      }
      window.activeMesh = e.object
    }
  
    const handlePointerEnter = e => {
      e.object.scale.x = 1.5
      e.object.scale.y = 1.5
      e.object.scale.z = 1.5
    }

    const handlePointerLeave = e => {
        if (!e.object.active) {
          scaleDown(e.object)
        }
      }
    
      const scaleDown = object => {
        object.scale.x = 1
        object.scale.y = 1
        object.scale.z = 1
      }
    
      return (
        <mesh
          ref={ref}
          api={api}
          {...props}
          castShadow
          onPointerDown={handlePointerDown}
          onPointerEnter={handlePointerEnter}
          onPointerLeave={handlePointerLeave}
        >
          <boxBufferGeometry args={[1, 1, 1]} />
          <meshPhysicalMaterial
            map={texture}
          />
        </mesh>
      )
    }

    export default Box;

ไฟล์ Draggable.jsx เพิ่มโค้ด

        controlsRef.current.addEventListener(
            'hoveron',
            e => scene.orbitControls.enabled = false
        ) 
        controlsRef.current.addEventListener(
            'hoveroff',
            e => scene.orbitControls.enabled = true
        ) 
        controlsRef.current.addEventListener(
            'dragstart',
            e => e.object.api.mass.set(0)
        )
        controlsRef.current.addEventListener(
            'dragend',
            e => e.object.api.mass.set(1)
        )
        controlsRef.current.addEventListener(
            'drag',
            e => {
                e.object.api.position.copy(e.object.position)
                e.object.api.velocity.set(0,0,0)
            }
        )   

ทดสอบ ยกกล่องขึ้นแล้วจะร่วงหล่นลงบนพื้น

โค้ดไฟล์ Draggable.jsx

import {
    DragControls
} from 'three/examples/jsm/controls/DragControls';
import { useRef, useEffect, useState } from 'react';
import { useThree, extend } from 'react-three-fiber';
extend({ DragControls });

const Draggable = props => {
    const groupRef = useRef();
    const controlsRef = useRef();
    const [children, setChildren] = useState([])
    const { camera, gl, scene } = useThree();

    useEffect(() => {
        setChildren(groupRef.current.children)     
    }, [])

    useEffect(() => {
        controlsRef.current.addEventListener(
            'hoveron',
            e => scene.orbitControls.enabled = false
        ) 
        controlsRef.current.addEventListener(
            'hoveroff',
            e => scene.orbitControls.enabled = true
        ) 
        controlsRef.current.addEventListener(
            'dragstart',
            e => e.object.api.mass.set(0)
        )
        controlsRef.current.addEventListener(
            'dragend',
            e => e.object.api.mass.set(1)
        )
        controlsRef.current.addEventListener(
            'drag',
            e => {
                e.object.api.position.copy(e.object.position)
                e.object.api.velocity.set(0,0,0)
            }
        )      
    }, [children])


    return (
        <group ref={groupRef}>
            <dragControls
                ref={controlsRef}
                args={[children, camera, gl.domElement]}
            />
            {props.children}
        </group>
    )
}

export default Draggable;

ผลลัพธ์การทำงาน

ดูผ่านเว็บไซต์ได้ที่

Leave a Reply

Your email address will not be published. Required fields are marked *