สร้างภาพสามมิติแบบโต้ตอบ (Interactive)
ในบทความที่แล้ว เราคุยกันเรื่องการสร้างพื้นผิว และในบทความนี้ เรากำลังจะทำ เพื่อพูดคุยเกี่ยวกับเหตุการณ์ (Events)
ข้อกำหนดเบื้องต้น
ข้อกำหนดสำหรับบทความนี้คือ คุณควรได้ทำตามบทความ การเพิ่มพื้นผิว (Adding Textures) มาก่อน
Events
มาเพิ่มเหตุการณ์การคลิกในกล่องของเรา และก่อนที่เราจะทำอย่างนั้น เราจะเปลี่ยนเรขาคณิตนี้กลับเป็นกล่อง เพื่อให้มันเป็นกล่องจริงๆ
<boxBufferGeometry args={[1, 1, 1]} />


แล้วเราจะเพิ่มกิจกรรม ผู้ฟังจะใช้จุดเดียว เราจะกำจัดเงาที่ได้รับนี้ออกไป
onPointerDown={handlePointerDown}
และเราจะสร้างฟังก์ชันที่เรียกว่า handlePointerDown และรับเหตุการณ์และเราแค่จะ console God เหมือนเหตุการณ์นั้น
const handlePointerDown = e => {
console.log(e)
}

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

เราจะเพิ่มกิจกรรมอีกสองสามที่นี่ เราจะใช้ onPointerEter และ onPointerLeave เราจะมีมันเพื่อที่ว่าเมื่อเมาส์ของเราเข้าไปในลูกบาศก์ มันจะขยายใหญ่ขึ้น และเมื่อเขาจากไป มันจะ ลดขนาดกลับลง เรามาตั้งค่าให้ onPointerEter เพื่อจัดการ handlePointerEnter จากนั้นเราจะตั้งค่า onPointerLeave เป็น handlePointerLeave
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}

ทีนี้ลองเขียนฟังก์ชันทั้งสองนี้กัน
มันจะมีเหตุการณ์ ดังนั้น จำไว้ว่า เราจะเข้าถึงตัวเมชได้เองภายในอ็อบเจกต์เหตุการณ์ ลองใช้แต่ละอ็อบเจกต์กัน จากนั้นเราจะตั้งค่ามาตราส่วน X, Y และ Z เราจะตั้งค่า X, Y และดูทีละรายการเพราะคุณสมบัติมาตราส่วนนั้นอ่านได้อย่างเดียว
นั่นเป็นเพียงบางสิ่งที่คุณต้องทำคือตั้งค่าคุณสมบัติ X, Y และ Z แยกกัน ก็เลยบอกตอนนี้ไปที่ 1.5
const handlePointerEnter = e => {
e.object.scale.x = 1.5
e.object.scale.y = 1.5
e.object.scale.z = 1.5
}

และรับสิ่งนี้สักครู่แล้วให้ทำซ้ำและเขียนฟังก์ชัน Hanno poignantly แล้วเวลาเราชี้เป้า และจะลดขนาดทุกอย่างกลับลงมาเป็นมาตราส่วนเดียว
const handlePointerLeave = e => {
if (!e.object.active) {
e.object.scale.x = 1
e.object.scale.y = 1
e.object.scale.z = 1
}
}

ดังนั้นเมื่อเมาส์เข้าสู่พื้นที่กล่องจะมีขนาดเพิ่มขึ้น และเมื่อออกจากกล่องไป มันจะลดขนาดลง


เราจะบันทึกวัตถุตาข่ายนี้ไปที่หน้าต่าง ลงในเนมสเปซหรือการจัดการสถานะบางอย่างที่ไม่รบกวนการแสดงส่วนประกอบของคุณ
ในฟังก์ชันจุดจับหรือลง ฉันจะบันทึกตาข่ายไปที่หน้าต่างภายใต้ตาข่ายที่เปิดใช้งานอยู่สมมุติว่า windows เป็น active mesh แล้วฉันจะส่งไปที่เหตุการณ์หรืออ็อบเจกต์ จากนั้นฉันจะตั้งค่าคุณสมบัติแอ็คทีฟบนเมชด้วยและจะเห็นว่าก่อนที่เราจะบันทึกลงใน window และ วัตถุใดวัตถุหนึ่งที่แอ็คทีฟเท่ากับจริง เอาล่ะ ดังนั้นหากฉันคลิกบนนี้ ตกลง เรายังคงพิมพ์มันในคอนโซล แต่ถ้าพิมพ์ window on active
const handlePointerDown = e => {
console.log(e)
e.object.active = true;
window.activeMesh = e.object
}

ดังนั้นจุดจัดการของเราจึงออกไป แต่สำหรับสิ่งนี้เท่านั้น หากเหตุการณ์หรืออ็อบเจ็กต์ทำงานอยู่ คุณสมบัติจะไม่ถูกตั้งค่าเป็นจริงดังนั้นถ้าไม่มีวัตถุอื่นทำงานอยู่ จากนั้นเราจะทำสิ่งนี้ ได้เลย เข้า ออก เข้า ออก แต่ถ้าเข้าแล้วคลิก แล้วปล่อยทิ้งไว้ให้ใหญ่ขึ้นเพราะฉันรู้ว่ามันเลือกแล้ว เอาล่ะ มีเหตุผลที่จะบันทึกวัตถุไปที่หน้าต่าง
เราจะไปที่นั้นในไม่กี่วินาที ขั้นแรก มาสร้างกล่องอื่นกัน ขอลงไปที่ผ้าใบก่อน แล้วจะจำลองกล่องนี่ตรงนี้ แล้วจะย้ายอันนี้ ไปทางซ้ายเล็กน้อยแล้วฉันจะย้ายอันใหม่ที่ฉันสร้างไปทางขวา บางทีผมอาจใส่อันนี้ไว้ที่ลบ 5 บนแกน X แล้วใส่อันใหม่ตอนห้า อาจไม่ใช่ห้า
เรา จะใส่มันสำหรับ- 4 และสี่ซ้ำกัน นี่บอกว่าตัวนี้คือ X -4
<Suspense fallback={null}>
<Box position={[-4, 1, 0]} />
</Suspense>
<Suspense fallback={null}>
<Box position={[4, 1, 0]} />
</Suspense>

และอันนี้พิเศษสำหรับ ตกลง เราจะดึงกล้องกลับมาตรงนี้ด้วย มาเปลี่ยนทั้งหมดนี้เป็น 7 กัน
camera={{ position: [7, 7, 7] }}


ตอนนี้ ถ้าต้องการเลือกอีกอันหนึ่ง และอยากให้อีกอันย่อขนาดกลับลงมา นั่นเป็นเหตุผลที่เราบันทึก Active Mesh ไว้ที่หน้าต่าง เพื่อที่ว่าเมื่อเลือกอันใหม่แล้ว เราสามารถตั้งค่าอีกอันให้ลดขนาดลงได้ มาจัดการกันเถอะ
ย้อนกลับไปในองค์ประกอบกล่องของเรา ในที่จับที่ชี้ลง ให้ตรวจสอบว่ามีเมชที่ใช้งานอยู่ที่กำหนดไว้ในหน้าต่างหรือไม่ แล้วถ้ามี เราจะลดขนาดเมชลงก่อนที่เราจะตั้งค่าแอกทีฟเมชใหม่ ดังนั้นถ้าเรารู้ว่า Active Mesh เราก็ลดขนาดลง แทนที่จะเขียนใหม่ เราจะแยกย่อยออกเป็นฟังก์ชันอื่น ฉันจะเรียกมันว่าย่อขนาดลงและมันจะดึงวัตถุ
ดังนั้นถ้าเรารู้จัก Active Mesh เราก็จะลดขนาดลง แทนที่จะเขียนสิ่งนี้ใหม่ ฉันจะแยกออกเป็นฟังก์ชันอื่น เราจะเรียกมันว่าลดขนาดลง และมันจะหยิบวัตถุ ผ่านนี้ e! จากนั้นที่นี่จะเรียกลดขนาดลง
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
}

แล้วเราจะเรียกมันว่าที่นี่ด้วย ดังนั้นจึงมีตาข่ายที่ใช้งานอยู่บนหน้าต่าง เราจะลดขนาดลง แล้วอีกอย่างที่เราต้องการทำคือตั้งค่าคุณสมบัติแอ็คทีฟที่เมชเป็นเท็จ
if (window.activeMesh) {
scaleDown(window.activeMesh)
window.activeMesh.active = false;
}

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

โค้ด
import './App.css';
import {
Canvas,
useFrame,
useThree,
extend,
useLoader
} from 'react-three-fiber';
import { useRef, Suspense } from 'react';
import {
OrbitControls
} from 'three/examples/jsm/controls/OrbitControls';
import * as THREE from 'three'
extend({ OrbitControls });
const Orbit = () => {
const { camera, gl } = useThree();
return (
<orbitControls args={[camera, gl.domElement]} />
)
}
const Box = props => {
const ref = useRef();
const texture = useLoader(
THREE.TextureLoader,
'/wood.jpg'
);
useFrame(state => {
ref.current.rotation.y += 0.01;
ref.current.rotation.x += 0.01;
});
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}
{...props}
castShadow
onPointerDown={handlePointerDown}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
<boxBufferGeometry args={[1, 1, 1]} />
<meshPhysicalMaterial
map={texture}
/>
</mesh>
)
}
const Background = props => {
const texture = useLoader(
THREE.TextureLoader,
'/autoshop.jpg'
);
const { gl } = useThree();
const formatted = new THREE.WebGLCubeRenderTarget(
texture.image.height
).fromEquirectangularTexture(gl, texture)
return (
<primitive
attach='background'
object={formatted}
/>
)
}
const Floor = props => {
return (
<mesh {...props} receiveShadow>
<boxBufferGeometry args={[20, 1, 10]} />
<meshPhysicalMaterial
/>
</mesh>
)
}
const Bulb = props => {
return (
<mesh {...props}>
<pointLight castShadow />
<sphereBufferGeometry args={[0.2, 20, 20]} />
<meshPhongMaterial emissive='yellow' />
</mesh>
)
}
function App() {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas
shadowMap
style={{ background: 'black' }}
camera={{ position: [7, 7, 7] }}
>
<ambientLight intensity={0.2} />
<Bulb position={[0, 3, 0]} />
<Orbit />
<axesHelper args={[5]} />
<Suspense fallback={null}>
<Box position={[-4, 1, 0]} />
</Suspense>
<Suspense fallback={null}>
<Box position={[4, 1, 0]} />
</Suspense>
<Suspense fallback={null}>
<Background />
</Suspense>
<Floor position={[0, -0.5, 0]} />
</Canvas>
</div>
);
}
export default App;
เราจะใส่ div ใหม่และกำหนดตำแหน่งที่แน่นอน แล้วจะให้ดัชนี Z เป็น 1 ด้วย แล้วใส่สี่เหลี่ยมสีลงใน div เช่น 50 พิกเซลคูณ 50 พิกเซล พร้อมพื้นหลังสีน้ำเงิน
<div style={{ position: 'absolute', zIndex: 1 }}>
<div
style={{ background: 'blue', height: 50, width: 50 }}>
</div>
</div>


และใช้อีกอันหนึ่งเป็นสีเหลือง แล้วทำอีกอันหนึ่งเป็นสีขาว เพราะนั่นเป็นสีเริ่มต้น นั่นจะทำให้มันกลับมาเป็นปกติ
<div style={{ position: 'absolute', zIndex: 1 }}>
<div
style={{
background: 'blue',
height: 50,
width: 50
}}
></div>
<div
style={{
background: 'yellow',
height: 50,
width: 50
}}
></div>
<div
style={{
background: 'white',
height: 50,
width: 50
}}
></div>


เราจะเพิ่มฟังก์ชันคลิกลงในช่องสี่เหลี่ยมสีเล็กๆ แต่ละช่อง เพื่อที่ว่าเมื่อเราคลิก มันจะกำหนดสีของตาข่ายปัจจุบันที่อยู่บนหน้าต่าง ดังนั้นเราจะบอกว่า onClick={handleClick}
<div style={{ position: 'absolute', zIndex: 1 }}>
<div
onClick={handleClick}
style={{
background: 'blue',
height: 50,
width: 50
}}
></div>
<div
onClick={handleClick}
style={{
background: 'yellow',
height: 50,
width: 50
}}
></div>
<div
onClick={handleClick}
style={{
background: 'white',
height: 50,
width: 50
}}
></div>

จากนั้นเราจะเขียนฟังก์ชัน handleClick
const handleClick = e => {
if (!window.activeMesh) return;
window.activeMesh.material.color = new THREE.Color(e.target.style.background)
}

ทดสอบคลิกที่กล่อง แล้วคลิกที่สีนำ้เงิน กล่องจะเปลี่ยนเป็นสีน้ำเงิน

ผลลัพธ์การทำงาน
โค้ด
import './App.css';
import {
Canvas,
useFrame,
useThree,
extend,
useLoader
} from 'react-three-fiber';
import { useRef, Suspense } from 'react';
import {
OrbitControls
} from 'three/examples/jsm/controls/OrbitControls';
import * as THREE from 'three'
extend({ OrbitControls });
const Orbit = () => {
const { camera, gl } = useThree();
return (
<orbitControls args={[camera, gl.domElement]} />
)
}
const Box = props => {
const ref = useRef();
const texture = useLoader(
THREE.TextureLoader,
'/wood.jpg'
);
useFrame(state => {
ref.current.rotation.y += 0.01;
ref.current.rotation.x += 0.01;
});
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}
{...props}
castShadow
onPointerDown={handlePointerDown}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
<boxBufferGeometry args={[1, 1, 1]} />
<meshPhysicalMaterial
map={texture}
/>
</mesh>
)
}
const Background = props => {
const texture = useLoader(
THREE.TextureLoader,
'/autoshop.jpg'
);
const { gl } = useThree();
const formatted = new THREE.WebGLCubeRenderTarget(
texture.image.height
).fromEquirectangularTexture(gl, texture)
return (
<primitive
attach='background'
object={formatted}
/>
)
}
const Floor = props => {
return (
<mesh {...props} receiveShadow>
<boxBufferGeometry args={[20, 1, 10]} />
<meshPhysicalMaterial
/>
</mesh>
)
}
const Bulb = props => {
return (
<mesh {...props}>
<pointLight castShadow />
<sphereBufferGeometry args={[0.2, 20, 20]} />
<meshPhongMaterial emissive='yellow' />
</mesh>
)
}
function App() {
const handleClick = e => {
if (!window.activeMesh) return;
window.activeMesh.material.color = new THREE.Color(e.target.style.background)
}
return (
<div style={{ height: '100vh', width: '100vw' }}>
<div style={{ position: 'absolute', zIndex: 1 }}>
<div
onClick={handleClick}
style={{
background: 'blue',
height: 50,
width: 50
}}
></div>
<div
onClick={handleClick}
style={{
background: 'yellow',
height: 50,
width: 50
}}
></div>
<div
onClick={handleClick}
style={{
background: 'white',
height: 50,
width: 50
}}
></div>
</div>
<Canvas
shadowMap
style={{ background: 'black' }}
camera={{ position: [7, 7, 7] }}
>
<ambientLight intensity={0.2} />
<Bulb position={[0, 3, 0]} />
<Orbit />
<axesHelper args={[5]} />
<Suspense fallback={null}>
<Box position={[-4, 1, 0]} />
</Suspense>
<Suspense fallback={null}>
<Box position={[4, 1, 0]} />
</Suspense>
<Suspense fallback={null}>
<Background />
</Suspense>
<Floor position={[0, -0.5, 0]} />
</Canvas>
</div>
);
}
export default App;
ดูผ่านเว็บไซต์ได้ที่