more posts

3D Scene with v0

Fun little 3D scene in React. Made with v0.dev.

31 August 2024azxyc

I saw some pretty cool examples generated by Vercel's v0 chat recently, so I decided to try it out for myself. Here's a 3D scene created using React Three Fiber. Click the cube!

Code


"use client"
import { Canvas, useFrame, useThree } from "@react-three/fiber"
import { OrbitControls, PerspectiveCamera, Box, useHelper } from "@react-three/drei"
import { Physics, usePlane, useBox } from "@react-three/cannon"
import * as THREE from "three"
import { useRef, useState } from "react"
import { DragControls } from './DragControls'

const GRAVITY_PRESETS = {
  earth: [0, -9.81, 0],
}

function Cube({ position, size, onSplit }) {
  const [ref, api] = useBox(() => ({
    mass: 1,
    position,
    args: [size, size, size],
  }))

  const handleClick = (event) => {
    event.stopPropagation()
    if (size > 0.1) {
      onSplit(position, size / 2)
      api.position.set(1000, 1000, 1000)
    }
  }

  return (
    <Box
      ref={ref}
      args={[size, size, size]}
      castShadow
      receiveShadow
      onClick={handleClick}
    >
      <meshPhysicalMaterial
        color="white"
        emissive="white"
        emissiveIntensity={0.2}
        roughness={0.1}
        metalness={0.1}
      />
    </Box>
  )
}

function CeilingLight() {
  const lightRef = useRef(null)
  useHelper(lightRef, THREE.DirectionalLightHelper, 1, "red")

  return (
    <directionalLight
      ref={lightRef}
      position={[0, 5, 0]}
      intensity={1}
      castShadow
      shadow-mapSize-width={2048}
      shadow-mapSize-height={2048}
    />
  )
}

function DragController({ children }) {
  const { camera, gl } = useThree()
  const groupRef = useRef(null)
  const controlsRef = useRef(null)

  useFrame(() => {
    if (controlsRef.current) {
      controlsRef.current.enabled = true
    }
  })

  return (
    <group ref={groupRef}>
      <primitive object={new DragControls([], camera, gl.domElement)} ref={controlsRef} />
      {children}
    </group>
  )
}

function Floor() {
  const [ref] = usePlane(() => ({
    rotation: [-Math.PI / 2, 0, 0],
    position: [0, -2, 0],
  }))

  return (
    <mesh ref={ref} receiveShadow>
      <planeGeometry args={[100, 100]} />
      <shadowMaterial transparent opacity={0.4} />
    </mesh>
  )
}

export default function Scene() {
  const [cubes, setCubes] = useState([{ position: [0, 0, 0], size: 1 }])
  const gravity = GRAVITY_PRESETS.earth

  const handleSplit = (position, newSize) => {
    const offsets = [
      [1, 1, 1], [-1, 1, 1], [1, -1, 1], [-1, -1, 1],
      [1, 1, -1], [-1, 1, -1], [1, -1, -1], [-1, -1, -1]
    ]
    const newCubes = offsets.map(offset => ({
      position: position.map((p, i) => p + offset[i] * newSize / 2),
      size: newSize
    }))
    setCubes(prev => [...prev, ...newCubes])
  }

  return (
    <div className="w-[500px] h-[500px] border border-primary/10 shadow-2xl shadow-white/5 rounded-2xl bg-black">
      <Canvas shadows>
        <color attach="background" args={["black"]} />
        <PerspectiveCamera makeDefault position={[3, 3, 3]} />
        <OrbitControls enableDamping dampingFactor={0.05} />
        <ambientLight intensity={0.1} />
        <CeilingLight />
        <Physics gravity={gravity}>
          <DragController>
            {cubes.map((cube, index) => (
              <Cube key={index} position={cube.position} size={cube.size} onSplit={handleSplit} />
            ))}
          </DragController>
          <Floor />
        </Physics>
      </Canvas>
    </div>
  )
}