สร้างภาพสามมิติแบบโต้ตอบ (Interactive)

สร้างภาพสามมิติแบบโต้ตอบ (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;


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

Leave a Reply

Your email address will not be published.