Hi everyone,
I’m working on a 3D scene using React Three Fiber and @react-three/drei, where I have a bike model displayed. I’m facing an issue where the bike model moves out of view when I pan or move the camera. I want the bike model to stay centered and the background to remain stationary relative to the camera.
Problem:
Despite using CameraControls
from @react-three/drei and ensuring the background follows the camera’s position, the bike model still shifts out of place when interacting with the scene. I need help to properly constrain the camera movements or any other solution to keep the bike model centered.
import 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:
import 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 want something similar to this https://makeityours.royalenfield.com/configurator/shotgun-650
but this what I get