Asked 8 months ago by PlutonianGuide429
How can I keep my React Three Fiber 3D bike model centered during camera movements?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 8 months ago by PlutonianGuide429
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Hi everyone,
I’m building a 3D scene using React Three Fiber and @react-three/drei with a bike model, but I’m facing an issue: when I pan or move the camera, the bike model shifts out of view even though the background remains aligned with the camera. I’m using CameraControls from @react-three/drei, and the background follows the camera’s position, yet the bike doesn’t stay centered. I’m looking for a way to constrain the camera movements or an alternative solution so the bike remains centered.
Below are the relevant code snippets from my project:
JAVASCRIPTimport React, { useState, useRef, useEffect, useCallback } from 'react'; import { Canvas, useFrame, useThree , } from '@react-three/fiber'; import { Center, Environment, PerspectiveCamera, CameraControls, Html,Caustics , useTexture } from '@react-three/drei'; import { EffectComposer, Bloom, ToneMapping } from '@react-three/postprocessing'; import * as THREE from 'three'; import { easing } from 'maath'; import { Bike5 } from './Bike5'; import { DefaultTank, Tank1, Tank2 } from './TankComponents'; import './App.css'; import Loading from './Loading'; import BikeColorSidebar from './BikeColorSidebar'; import { ToneMappingMode } from 'postprocessing'; function GlowingRing() { const ringRef = useRef(); const lightRef = useRef(); const materialRef = useRef(); useFrame((state) => { const t = (1 + Math.sin(state.clock.elapsedTime * 2)) / 2; if (materialRef.current) { materialRef.current.emissiveIntensity = 4.2; } if (lightRef.current) { lightRef.current.intensity = 1 + t * 4; } }); return ( <group rotation={[-Math.PI / 2, 0, 0]} position={[0, -5, 0]}> <mesh ref={ringRef}> <ringGeometry args={[12.6, 12.8, 500]} /> <meshStandardMaterial ref={materialRef} emissive={new THREE.Color("#0000ff")} emissiveIntensity={1} toneMapped={false} /> </mesh> <pointLight ref={lightRef} color={[1, 0.1, 0.1]} intensity={1} distance={5} /> </group> ); } function GlowingBlub() { const blubRef = useRef(); const lightRef = useRef(); const materialRef = useRef(); useFrame((state) => { const t = (1 + Math.sin(state.clock.elapsedTime * 2)) / 2; if (materialRef.current) { materialRef.current.emissiveIntensity = 4.2; } if (lightRef.current) { lightRef.current.intensity = 1 + t * 4; } }); return ( <group rotation={[-Math.PI / 2, 0, 0]} position={[0, 5, 0]}> <mesh ref={blubRef}> <ringGeometry args={[1.6, 1.8, 500]} /> <meshStandardMaterial ref={materialRef} emissive={new THREE.Color("white")} emissiveIntensity={1} toneMapped={false} /> </mesh> </group> ); } function Background() { const texture = useTexture("./AdobeStock_427329591.jpeg"); // Replace with your background image path return ( <mesh scale={[-1, 1, 1]}> <sphereGeometry args={[500, 60, 40]} /> <meshBasicMaterial map={texture} side={THREE.BackSide} /> </mesh> ); } function CameraLogger() { const { camera } = useThree(); useFrame(() => { console.log('Camera position:', camera.position); console.log('Camera rotation:', camera.rotation); }); return null; } function App() { const [color, setColor] = useState(null); const [resetToDefault, setResetToDefault] = useState(false); const [selectedBikeColor, setSelectedBikeColor] = useState('default'); const [selectedTank, setSelectedTank] = useState('default'); const [loading, setLoading] = useState(true); const [totalCost, setTotalCost] = useState(5000); // Base cost of the bike const cameraControlsRef = useRef(); useEffect(() => { const timer = setTimeout(() => setLoading(false), 1000); // Simulated loading return () => clearTimeout(timer); }, []); useEffect(() => { // Calculate total cost let cost = 5000; // Base cost if (color) cost += 400; // Add 400 for custom color if (selectedTank !== 'default') cost += 200; // Add 200 for custom tank setTotalCost(cost); }, [color, selectedTank]); const handleColorChange = useCallback((newColor) => { setResetToDefault(false); setColor(newColor); setSelectedBikeColor('default'); cameraControlsRef.current?.setLookAt(8, 0, 19, 0, 0, 0, true); }, []); const handleReset = useCallback(() => { setResetToDefault(true); setSelectedBikeColor('default'); setSelectedTank('default'); setColor(null); cameraControlsRef.current?.setLookAt(8, 0, 19, 0, 0, 0, true); }, []); const handleBikeColorChange = useCallback((bikeColor) => { setResetToDefault(false); setSelectedBikeColor(bikeColor); setColor(null); cameraControlsRef.current?.setLookAt(8, 0, 19, 0, 0, 0, true); }, []); const handleTankChange = useCallback((tankOption) => { setSelectedTank(tankOption); cameraControlsRef.current?.setLookAt(6, 1, 1, 0, 0, 0, true); }, []); const getTankComponent = useCallback(() => { switch (selectedTank) { case 'tank1': return Tank1; case 'tank2': return Tank2; default: return DefaultTank; } }, [selectedTank]); return ( <div className="app-container"> <div className={`canvas-container ${loading ? 'hidden' : ''}`}> {loading && <Loading />} <Canvas shadows> <PerspectiveCamera makeDefault position={[0, 0, 0]} fov={40} /> <CameraControls ref={cameraControlsRef} minPolarAngle={Math.PI / 2.7} maxPolarAngle={Math.PI / 2} minDistance={4.5} maxDistance={20} /> <color attach="background" args={['#15151a']} /> <ambientLight intensity={0.2} /> <directionalLight position={[5, 5, 5]} intensity={0.5} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} shadow-camera-far={50} shadow-camera-left={-10} shadow-camera-right={10} shadow-camera-top={10} shadow-camera-bottom={-10} /> <Center top position={[0, -8, 0]} scale={1.05} castShadow> <Bike5 color={color} resetToDefault={resetToDefault} selectedBikeColor={selectedBikeColor} TankComponent={getTankComponent()} /> </Center> <GlowingRing /> <mesh rotation={[-Math.PI / 2, 0, 0]} position={[2, -7.8, 0]} receiveShadow > <planeGeometry args={[30, 30]} /> <shadowMaterial opacity={0.8} /> </mesh> <Environment preset='city' /> <Background/> <EffectComposer> <Bloom luminanceThreshold={1.5} mipmapBlur intensity={0.5} mode={ToneMappingMode.ACES_FILMIC}/> <ToneMapping /> </EffectComposer> <CameraLogger /> <Html> </Html> </Canvas> </div> <div className="cost-overlay"> Total Cost: ${totalCost} </div> <BikeColorSidebar handleBikeColorChange={handleBikeColorChange} handleColorChange={handleColorChange} handleTankChange={handleTankChange} handleReset={handleReset} currentColor={color} currentBikeColor={selectedBikeColor} currentTank={selectedTank} /> </div> ); } export default App;
Bike.js:
JAVASCRIPTimport React, { useEffect, useState, useMemo, useCallback } from 'react'; import { useGLTF } from '@react-three/drei'; import * as THREE from 'three'; import { TextureLoader } from 'three'; import { useLoader } from '@react-three/fiber'; export function Bike5({ color, resetToDefault, selectedBikeColor, TankComponent }) { const { nodes, materials } = useGLTF('/Bike5/Bike5-transformed.glb'); // Load all textures at the top level const metalPlatesMap = useLoader(TextureLoader, './Textures/Metal/MetalPlates007_4K_Color.png'); const metalPlatesNormalMap = useLoader(TextureLoader, './Textures/Metal/MetalPlates007_4K_NormalGL.png'); const metalPlatesDisplacementMap = useLoader(TextureLoader, './Textures/Metal/MetalPlates007_4K_Displacement.png'); const metalPlatesMetalnessMap = useLoader(TextureLoader, './Textures/Metal/MetalPlates007_4K_Metalness.png'); const metalPlatesRoughnessMap = useLoader(TextureLoader, './Textures/Metal/MetalPlates007_4K_Roughness.png'); const metal007Map = useLoader(TextureLoader, './Textures/Metal2/Metal007_4K_Color.png'); const metal007NormalMap = useLoader(TextureLoader, './Textures/Metal2/Metal007_4K_NormalGL.png'); const metal007DisplacementMap = useLoader(TextureLoader, './Textures/Metal2/Metal007_4K_Displacement.png'); const metal007MetalnessMap = useLoader(TextureLoader, './Textures/Metal2/Metal007_4K_Metalness.png'); const metal007RoughnessMap = useLoader(TextureLoader, './Textures/Metal2/Metal007_4K_Roughness.png'); const marble009Map = useLoader(TextureLoader, './Textures/Metal3/Marble009_4K_Color.png'); const marble009NormalMap = useLoader(TextureLoader, './Textures/Metal3/Marble009_4K_NormalGL.png'); const marble009DisplacementMap = useLoader(TextureLoader, './Textures/Metal3/Marble009_4K_Displacement.png'); const marble009RoughnessMap = useLoader(TextureLoader, './Textures/Metal3/Marble009_4K_Roughness.png'); const leatherSeatMap = useLoader(TextureLoader, './Textures/Seat2/Leather005_4K_Color.png'); const leatherSeatNormalMap = useLoader(TextureLoader, './Textures/Seat2/Leather005_4K_NormalGL.png'); const leatherSeatDisplacementMap = useLoader(TextureLoader, './Textures/Seat2/Leather005_4K_Displacement.png'); const leatherSeatRoughnessMap = useLoader(TextureLoader, './Textures/Seat2/Leather005_4K_Roughness.png'); const bikeTextures = useMemo(() => ({ metalPlates: { map: metalPlatesMap, normalMap: metalPlatesNormalMap, displacementMap: metalPlatesDisplacementMap, metalnessMap: metalPlatesMetalnessMap, roughnessMap: metalPlatesRoughnessMap, }, metal007: { map: metal007Map, normalMap: metal007NormalMap, displacementMap: metal007DisplacementMap, metalnessMap: metal007MetalnessMap, roughnessMap: metal007RoughnessMap, }, marble009: { map: marble009Map, normalMap: marble009NormalMap, displacementMap: marble009DisplacementMap, roughnessMap: marble009RoughnessMap, }, leatherSeat: { map: leatherSeatMap, normalMap: leatherSeatNormalMap, displacementMap: leatherSeatDisplacementMap, roughnessMap: leatherSeatRoughnessMap, }, }), [ metalPlatesMap, metalPlatesNormalMap, metalPlatesDisplacementMap, metalPlatesMetalnessMap, metalPlatesRoughnessMap, metal007Map, metal007NormalMap, metal007DisplacementMap, metal007MetalnessMap, metal007RoughnessMap, marble009Map, marble009NormalMap, marble009DisplacementMap, marble009RoughnessMap, leatherSeatMap, leatherSeatNormalMap, leatherSeatDisplacementMap, leatherSeatRoughnessMap ]); const getRandomTexture = useCallback(() => { const textures = Object.values(bikeTextures); return textures[Math.floor(Math.random() * textures.length)]; }, [bikeTextures]); const [currentTextures, setCurrentTextures] = useState({ frame: bikeTextures.metalPlates, panels: bikeTextures.metal007, accents: bikeTextures.marble009, seat: bikeTextures.leatherSeat, }); useEffect(() => { if (resetToDefault) { setCurrentTextures({ frame: bikeTextures.metalPlates, panels: bikeTextures.metal007, accents: bikeTextures.marble009, seat: bikeTextures.leatherSeat, }); } else { switch (selectedBikeColor) { case 'BikeColor1': case 'BikeColor2': case 'BikeColor3': setCurrentTextures({ frame: getRandomTexture(), panels: getRandomTexture(), accents: getRandomTexture(), seat: bikeTextures.leatherSeat, }); break; default: setCurrentTextures(prevTextures => ({ ...prevTextures })); break; } } }, [selectedBikeColor, resetToDefault, bikeTextures, getRandomTexture]); const createMaterial = (textures) => { return new THREE.MeshStandardMaterial({ ...textures, displacementScale: 0.05, color: color || undefined }); }; return ( <group dispose={null} castShadow> <mesh castShadow receiveShadow geometry={nodes.Chain_transmission.geometry} material={createMaterial(currentTextures.frame)} position={[11.371, 0.891, -10.938]} rotation={[2.896, 0.008, -Math.PI]} scale={0.01} /> <mesh castShadow receiveShadow geometry={nodes.FRONT_disc_.geometry} material={createMaterial(currentTextures.panels)} position={[10.8, 0.802, 12.375]} rotation={[0, 1.566, 0]} scale={0.01} /> <TankComponent material={createMaterial(currentTextures.accents)} color={color} textures={currentTextures} /> <mesh castShadow receiveShadow geometry={nodes.front_headlamp_for_blender.geometry} material={createMaterial(currentTextures.panels)} position={[10.133, -1.186, -3.42]} rotation={[-Math.PI, 1.562, -Math.PI]} scale={0.1} /> <mesh castShadow receiveShadow geometry={nodes.front_headlamp_for_blender001.geometry} material={materials['Cool glass']} position={[10.133, -1.186, -3.42]} rotation={[-Math.PI, 1.562, -Math.PI]} scale={0.1} /> <mesh castShadow receiveShadow geometry={nodes.HEAD_LIGHT_MOUNT_75_mm.geometry} material={createMaterial(currentTextures.frame)} position={[9.347, 5.994, -0.528]} rotation={[1.067, 0.036, 1.497]} scale={0.01} /> <mesh castShadow receiveShadow geometry={nodes.FRONT_WHEEL.geometry} material={materials.Tyre} position={[9.537, -1.555, 5.303]} scale={0.01} /> <mesh castShadow receiveShadow geometry={nodes.seat3.geometry} material={new THREE.MeshStandardMaterial({ ...currentTextures.seat, displacementScale: 0.05, roughness: 0.9, metalness: 0.1, color: color || undefined })} position={[9.233, 7.211, -4.904]} rotation={[1.59, 0.014, -1.567]} scale={0.01} /> <mesh castShadow receiveShadow geometry={nodes.REAR_WHEEL001.geometry} material={materials.Tyre} position={[9.2, -1.885, -7.132]} rotation={[0, -0.012, 0]} scale={0.01} /> <mesh castShadow receiveShadow geometry={nodes.REAR_WHEEL003.geometry} material={materials.Tyre} position={[9.2, -1.885, -7.132]} rotation={[0, -0.012, 0]} scale={0.01} /> </group> ); } useGLTF.preload('/Bike5/Bike5-transformed.glb'); export default Bike5;
I’m aiming for an experience similar to https://makeityours.royalenfield.com/configurator/shotgun-650, but this is what I get:
Comments 0
•Answers 1
•Views 194
Answer by TychoAI Agent • 9 days ago
Based on your issue where the bike model shifts out of view while the background stays aligned with the camera, I recommend switching from CameraControls
to TrackballControls
from Three.js.
TrackballControls is similar to OrbitControls but doesn't maintain a fixed "up" vector, which gives you more flexibility in camera rotations. This is particularly useful for product configurators where you want the object to remain centered while allowing the user to freely rotate around it.
Here's how to modify your code to use TrackballControls:
JAVASCRIPTimport { TrackballControls } from 'three/examples/jsm/controls/TrackballControls' import { extend, useThree, useFrame } from '@react-three/fiber' // Extend TrackballControls to make it available in JSX extend({ TrackballControls }) function Controls() { const { camera, gl } = useThree() const controlsRef = useRef() useFrame(() => { controlsRef.current.update() }) return ( <trackballControls ref={controlsRef} args={[camera, gl.domElement]} target={[0, -8, 0]} // Target the bike's position rotateSpeed={2.0} zoomSpeed={1.2} panSpeed={0.8} noZoom={false} noPan={false} staticMoving={true} dynamicDampingFactor={0.3} /> ) } // Then in your App component, replace CameraControls with: <Canvas shadows> <PerspectiveCamera makeDefault position={[8, 0, 19]} fov={40} /> <Controls /> {/* Rest of your scene */} </Canvas>
If you prefer to keep using CameraControls, you can modify it to always target the bike:
JAVASCRIPTfunction App() { // Your existing code... return ( <div className="app-container"> <div className={`canvas-container ${loading ? 'hidden' : ''}`}> {loading && <Loading />} <Canvas shadows> <PerspectiveCamera makeDefault position={[8, 0, 19]} fov={40} /> <CameraControls ref={cameraControlsRef} minPolarAngle={Math.PI / 2.7} maxPolarAngle={Math.PI / 2} minDistance={4.5} maxDistance={20} truckSpeed={0} // Disable panning dollyToCursor={false} // Always target the bike position target={[0, -8, 0]} /> {/* Rest of your scene */} </Canvas> </div> </div> ); }
Your bike model appears to be positioned with an offset. Make sure the bike is properly centered:
JAVASCRIPT<Center position={[0, -8, 0]} scale={1.05} castShadow> <Bike5 color={color} resetToDefault={resetToDefault} selectedBikeColor={selectedBikeColor} TankComponent={getTankComponent()} /> </Center>
For your reset and color change functions, update them to properly target the bike:
JAVASCRIPTconst handleReset = useCallback(() => { setResetToDefault(true); setSelectedBikeColor('default'); setSelectedTank('default'); setColor(null); // Target the bike's position cameraControlsRef.current?.setLookAt(8, 0, 19, 0, -8, 0, true); }, []);
These changes should help keep your bike model centered in the view while allowing users to rotate around it, similar to the Royal Enfield configurator you referenced.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
No comments yet.