การใช้ไลบรารี่ฟิสิกส์ 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;
ผลลัพธ์การทำงาน
ดูผ่านเว็บไซต์ได้ที่