April 10, 2025
AntiThree(1) - furniture
Shopping - furniture (Anti)
🔥 可利用概念
- 物體 raycasting 選擇物體
- 滑鼠對於 camera 的交互效果
- matth 數字的動畫效果
- 場景優化, 包含光影 / Bvh / 色彩校正等等

內容: 透過 Hover 家具同時顯示對應價格.
mattheasing 做到動畫效果<Bvh>提供內容優化 raycasting 的效能<Selection>提供 raycasting, 需配合 mesh 外包裹<Select>使用<Outline>提供 raycasting 對應的物品顯示效果<TiltShift2>提供霧化後處理效果<ToneMapping />自動調整顏色與亮度對比,讓場景自然飽和, 更接近攝影效果<Mask>這個很迷, 還需要多案例了解<N8AO>增加立體感, 在物體周邊的陰影- 透過 滑鼠位置去找角度與位置, 更新 camera 位置 (真實場景可以考慮禁用, 直接提供不同視角切換按鈕即可)
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)
})

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>
)
}