April 10, 2025

AntiThree(1) - furniture


Shopping - furniture (Anti)

🔥 可利用概念

clipboard.png

內容: 透過 Hover 家具同時顯示對應價格.

  useFrame((state, delta) => {
    easing.damp3(state.camera.position, [state.pointer.x, 1 + state.pointer.y / 2, 8 + Math.atan(state.pointer.x * 2)], 0.3, delta)
    state.camera.lookAt(state.camera.position.x * 0.9, 0, -4)
  })

clipboard.png

import * as THREE from "three"
import { easing } from "maath"
import { Canvas, useFrame, useThree } from "@react-three/fiber"
import { Sky, Bvh } from "@react-three/drei"
import { EffectComposer, Selection, Outline, N8AO, TiltShift2, ToneMapping } from "@react-three/postprocessing"
import { Scene } from "./Scene"

export const App = () => (
 // dpr(device pixel ratio): 設置畫布的像素比例,這裡設置為1,以避免高像素比例引起的模糊問題
 // flat: 關閉色彩校正, 不讓 r3f 的預設色彩校正影響
 <Canvas flat dpr={1} gl={{ antialias: false }} camera={{ position: [0, 1, 6], fov: 25, near: 1, far: 20 }}>
   {/* 提供環境光源 */}
   <ambientLight intensity={1.5 * Math.PI} />
   {/* <Sky /> */}
   {/* Bounding Volume Hierarchy: 用於檢測射線, firstHitOnly 則是只抓第一次命中的物體 */}
   <Bvh firstHitOnly>
     {/* 在 Bvh 以下的內容可以被優化, selection 主要處理 選中事件 對應 Effect內的 Outline */}
     <Selection>
       {/* 針對畫面做後處理- 所有效果 */}
       <Effects />
       <Scene rotation={[0, Math.PI / 2, 0]} position={[0, -1, -0.85]} />
     </Selection>
   </Bvh>
 </Canvas>
)

function Effects() {
 const { size } = useThree()
 useFrame((state, delta) => {
   easing.damp3(state.camera.position, [state.pointer.x, 1 + state.pointer.y / 2, 8 + Math.atan(state.pointer.x * 2)], 0.3, delta)
   state.camera.lookAt(state.camera.position.x * 0.9, 0, -4)
 })
 return (
   // stencilBuffer 讓效果能只作用在 <Selection> 中被 <Select> 包住的物件上
   // disableNormalPass 不渲染法線圖,可略微提昇效能
   // autoClear 多效果疊加時不要每次自動清除畫面 => true 影響 selection 效果
   // multisampling 提高抗鋸齒品質(4x MSAA)
   <EffectComposer stencilBuffer disableNormalPass autoClear={false} multisampling={4}>
     {/* 在每個建模之間加入 陰影, 強化立體感 */}
     <N8AO halfRes aoSamples={5} aoRadius={0.4} distanceFalloff={0.75} intensity={1} />
     {/* 直接影響 Selection 裡面的選中效果  */}
     <Outline visibleEdgeColor="red" hiddenEdgeColor="white" blur width={size.width * 1.25} edgeStrength={10} />
     {/* 週邊模糊效果使用 */}
     <TiltShift2 samples={5} blur={0.1} />
     {/* 自動調整顏色與亮度對比,讓場景自然飽和, 更接近攝影效果 */}
     <ToneMapping />
   </EffectComposer>
 )
}
import { useState, useCallback } from "react"
import { debounce } from "lodash"
import { useGLTF, useEnvironment, Text } from "@react-three/drei"
import { Select } from "@react-three/postprocessing"
import { Price } from "./Price"

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.2.16 kitchen.glb --transform
Files: kitchen.glb [134.46MB] > kitchen-transformed.glb [2.1MB] (98%)
*/
export function Scene(props) {
  // Load model
  const { nodes, materials } = useGLTF("/kitchen-transformed.glb")
  // Load environment (using it only on the chairs, for reflections)
  const env = useEnvironment({ preset: "city" })
  // Hover state
  const [hovered, hover] = useState(null)
  // Debounce hover a bit to stop the ticker from being erratic
  const debouncedHover = useCallback(debounce(hover, 30), [])
  const over = (name) => (e) => (e.stopPropagation(), debouncedHover(name))
  // Get the priced item
  const price = { KNOXHULT: 5999, BRÖNDEN: 433, SKAFTET: 77, FANBYN: 255, VOXLÖV: 1699, LIVSVERK: 44 }[hovered] ?? 5999
  return (
    <>
      <group {...props}>
        <mesh geometry={nodes.vase1.geometry} material={materials.gray} material-envMap={env} />
        <mesh geometry={nodes.bottle.geometry} material={materials.gray} material-envMap={env} />
        <mesh geometry={nodes.walls_1.geometry} material={materials.floor} />
        <mesh geometry={nodes.walls_2.geometry} material={materials.walls} />
        <mesh geometry={nodes.plant_1.geometry} material={materials.potted_plant_01_leaves} />
        <mesh geometry={nodes.plant_2.geometry} material={materials.potted_plant_01_pot} />
        <mesh geometry={nodes.cuttingboard.geometry} material={materials.walls} />
        <mesh geometry={nodes.bowl.geometry} material={materials.walls} />
        {/* select 配合外部 selection 變成可選 */}
        <Select enabled={hovered === "BRÖNDEN"} onPointerOver={over("BRÖNDEN")} onPointerOut={() => debouncedHover(null)}>
          <mesh geometry={nodes.carpet.geometry} material={materials.carpet} />
        </Select>
        <Select enabled={hovered === "VOXLÖV"} onPointerOver={over("VOXLÖV")} onPointerOut={() => debouncedHover(null)}>
          <mesh geometry={nodes.table.geometry} material={materials.walls} material-envMap={env} material-envMapIntensity={0.5} />
        </Select>
        <Select enabled={hovered === "FANBYN"} onPointerOver={over("FANBYN")} onPointerOut={() => debouncedHover(null)}>
          <mesh geometry={nodes.chairs_1.geometry} material={materials.walls} />
          <mesh geometry={nodes.chairs_2.geometry} material={materials.plastic} material-color="#1a1a1a" material-envMap={env} />
        </Select>
        <Select enabled={hovered === "LIVSVERK"} onPointerOver={over("LIVSVERK")} onPointerOut={() => debouncedHover(null)}>
          <mesh geometry={nodes.vase.geometry} material={materials.gray} material-envMap={env} />
        </Select>
        <Select enabled={hovered === "SKAFTET"} onPointerOver={over("SKAFTET")} onPointerOut={() => debouncedHover(null)}>
          <mesh geometry={nodes.lamp_socket.geometry} material={materials.gray} material-envMap={env} />
          <mesh geometry={nodes.lamp.geometry} material={materials.gray} />
          <mesh geometry={nodes.lamp_cord.geometry} material={materials.gray} material-envMap={env} />
        </Select>
        <mesh geometry={nodes.kitchen.geometry} material={materials.walls} />
        <mesh geometry={nodes.sink.geometry} material={materials.chrome} material-envMap={env} />
      </group>
      <Text position={[1, 1.25, 0]} color="black" fontSize={0.15} font="Inter-Regular.woff" letterSpacing={-0.05}>
        {hovered ? hovered : "KNOXHULT"}
      </Text>
      {/** Forward the price to the ticker component */}
      <Price value={price} position={[-2, 0.3, -3.25]} />
    </>
  )
}
import { useRef } from "react"
import { easing } from "maath"
import { useFrame } from "@react-three/fiber"
import { Text, Mask, useMask } from "@react-three/drei"

export const Price = ({ value, currency = "$", ...props }) => {
  return (
    <group {...props}>
      {[...`✨✨✨${value}`.slice(-4)].map((num, index) => (
        <Counter index={index} value={num === "" ? -1 : num} key={index} speed={0.1 * (4 - index)} stencil={stencil} />
      ))}
      <Text children={currency} anchorY="bottom" position={[4 * 1.1, -0.25, 0]} fontSize={1} font="Inter-Regular.woff" />
      <Mask id={1}>
        <planeGeometry args={[10, 1.55]} />
      </Mask>
    </group>
  )
}

function Counter({ index, value, speed = 0.1 }) {
  const stencil = useMask(1)
  const ref = useRef()
  useFrame((state, delta) => easing.damp(ref.current.position, "y", value * -2, speed, delta))
  return (
    <group position-x={index * 1.1} ref={ref}>
      {/* counter 總共每一排都有 0-9, 依照位置 number 排定 y, 透過 useFrame 做 easing 過度, value 提供高度差異 */}
      {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((number) => (
        <Text key={number} position={[0, number * 2, 0]} fontSize={2} font="Inter-Regular.woff">
          {number}
          <meshBasicMaterial {...stencil} />
        </Text>
      ))}
    </group>
  )
}