Hello,I don’t know if this is really how it should be,but it’s definitely not what I was expecting.I’ve looked through,and didn’t see anyone else having this problem.
Briefly, when I load a model and try to change the preset in Env, the whole scene is re-rendered, including the useEffect of loading the model again, but if I have been on a preset before, say I was on sunset, and now forest, then when I switch back to sunset everything is fine, nothing is reloaded.
Viewer cod
'use client'
import { Cache, Color, Light, PCFSoftShadowMap } from 'three'
import {
AccumulativeShadows,
Center,
Environment,
GizmoHelper,
GizmoViewport,
Grid,
RandomizedLight,
} from '@react-three/drei'
import Lights from './Lights/Lights'
import ModelHandler from './ModelSettings/ModelHandler'
import { Stats } from '@react-three/drei'
import { TransformControls, OrbitControls, Shadow } from '@react-three/drei'
import useModelLoader from './ModelSettings/ModelLoader'
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'
import React, {
useEffect,
useRef,
useState,
useMemo,
createContext,
useContext,
} from 'react'
import { MyProvider } from '../MyContext'
import Env from './Environment'
import { memo } from 'react'
import {
Leva,
useControls,
LevaPanel,
LevaStoreProvider,
useStoreContext,
useCreateStore,
folder,
} from 'leva'
import { environments } from './environments'
import { removeListener } from 'process'
import { FBXLoader } from 'three/examples/jsm/Addons.js'
import { contain } from 'three/src/extras/TextureUtils.js'
import { Canvas, render, useThree } from '@react-three/fiber'
interface ViewerProps {
url: string
rootPath: string
fileMap: Map<string, File>
options?: {
kiosk?: boolean
preset?: string
cameraPosition?: number[] | null
}
rootFile: File | string
fileType: string
}
const Shadows = memo(() => (
<AccumulativeShadows
temporal
frames={100}
color='#9d4b4b'
colorBlend={0.5}
alphaTest={0.9}
scale={20}
>
<RandomizedLight amount={8} radius={4} position={[5, 5, -10]} />
</AccumulativeShadows>
))
function SceneBackground({ backgroundStore }: any) {
const { scene } = useThree()
const { ColorType } = useControls(
{ ColorType: '#191919' },
{ store: backgroundStore }
)
useEffect(() => {
scene.background = new Color(ColorType)
}, [ColorType])
return null
}
const GridChange = ({ circleSize, segments }: any) => {
// const { gridSize, ...gridConfig } = useControls({
// gridSize: [10.5, 10.5],
// cellSize: { value: 0.6, min: 0, max: 10, step: 0.1 },
// cellThickness: { value: 1, min: 0, max: 5, step: 0.1 },
// cellColor: '#6f6f6f',
// sectionSize: { value: 3.3, min: 0, max: 10, step: 0.1 },
// sectionThickness: { value: 1.5, min: 0, max: 5, step: 0.1 },
// sectionColor: '#9d4b4b',
// fadeDistance: { value: 25, min: 0, max: 100, step: 1 },
// fadeStrength: { value: 1, min: 0, max: 1, step: 0.1 },
// followCamera: false,
// infiniteGrid: true,
// })
// return <Grid position={[0, -0.01, 0]} args={gridSize} {...gridConfig} />
return (
<mesh rotation-x={-Math.PI / 2} receiveShadow>
<circleGeometry args={[circleSize, segments]} />
<meshStandardMaterial />
</mesh>
)
}
const GridHelperChange = ({ gridSize }: any) => {
return <gridHelper args={[gridSize]} position={[0, -0.01, 0]} />
}
const GridOrHelper = ({ gridChangeStore }: any) => {
const { Change } = useControls({ Change: true }, { store: gridChangeStore })
const GridVal = useControls(
{
'Grid Helper': folder(
{
gridSize: 10,
},
{ render: get => !get('Change') }
),
},
{ store: gridChangeStore }
)
// Standard grid controls - only rendered when Change is true
const CircleVal = useControls(
'',
{
'Circle Settings': folder(
{
circleSize: 10,
segment: 10,
cellColor: '#6f6f6f',
// Add other grid controls
},
{ render: get => get('Change') }
),
},
{ store: gridChangeStore }
)
return Change ? (
<GridChange
circleSize={CircleVal.circleSize}
segments={CircleVal.segment}
/>
) : (
<GridHelperChange gridSize={GridVal.gridSize} />
)
}
interface PanelConfig {
id: string
title: string
Component?: React.FC<{ store: any }>
store: any
isCollapsed: boolean
}
interface Panel {
id: string
store: any
isCollapsed: boolean
}
const Viewer = ({
url,
rootPath,
fileMap,
options,
rootFile,
fileType,
}: ViewerProps) => {
const panelConfigs: PanelConfig[] = [
{
id: 'grid',
title: 'Grid Controls',
store: useCreateStore(),
isCollapsed: true,
Component: GridOrHelper,
},
{
id: 'background',
title: 'Scene Settings',
store: useCreateStore(),
isCollapsed: true,
Component: SceneBackground,
},
{
id: 'transform',
title: 'Transform Controls',
store: useCreateStore(),
isCollapsed: true,
Component: ModelHandler,
},
{
id: 'model',
title: 'Model Settings',
isCollapsed: true,
store: useCreateStore(),
},
]
// Initialize panels with stores and collapse state
const [panels, setPanels] = useState<PanelConfig[]>(panelConfigs)
// update panel state
const updatePanelState = (panelId: string, isCollapsed: boolean) => {
setPanels(prevPanels =>
prevPanels.map(panel =>
panel.id === panelId ? { ...panel, isCollapsed } : panel
)
)
}
// Получить store по id
const getStoreById = (id: string) =>
panels.find(panel => panel.id === id)?.store
const renderPanel = ({ id, title }: PanelConfig) => {
const panel = panels.find(p => p.id === id)
if (!panel) return null
return (
<LevaPanel
key={id}
store={panel.store}
titleBar={{
drag: false,
title: title,
}}
fill
flat
collapsed={{
collapsed: panel.isCollapsed,
onChange: state => updatePanelState(id, state),
}}
/>
)
}
return (
<div className='h-full w-full relative'>
<Canvas
shadows
gl={{
antialias: false,
}}
>
<SceneBackground backgroundStore={getStoreById('background')} />
<MyProvider value={{ url, rootPath, fileMap, fileType, rootFile }}>
<ModelHandler
key={`${url}-${rootPath}-${fileType}-${rootFile}`}
transformStore={getStoreById('transform')}
modelStore={getStoreById('model')}
/>
</MyProvider>
<Env envStore={getStoreById('background')} />
<Lights lightStore={getStoreById('background')} />
<OrbitControls makeDefault />
<GridOrHelper gridChangeStore={getStoreById('grid')} />
<GizmoHelper alignment='bottom-center' margin={[80, 80]}>
<GizmoViewport
axisColors={['red', 'green', 'blue']}
labelColor='white'
/>
</GizmoHelper>
<Stats />
</Canvas>
<div
className='absolute top-0 right-0 w-80 h-full bg-black/20 backdrop-blur-sm'
style={{
maxHeight: 'calc(100vh - 5rem)',
overflowY: 'auto',
overflowX: 'hidden',
}}
>
<div className='flex flex-col gap-2 p-2'>
{panelConfigs.map(renderPanel)}
</div>
</div>
</div>
)
}
export default Viewer
Env code
import React, { memo, useCallback } from 'react'
import { Environment } from '@react-three/drei'
import { useControls } from 'leva'
const Env = memo(({ envStore }: any) => {
// Move state to component level to prevent re-renders
const [isVisible, setIsVisible] = React.useState(true)
const controls = useControls(
'Ground',
{
'off/on': {
value: true,
onChange: useCallback((v: any) => setIsVisible(v), []),
},
height: {
value: 10,
min: 0,
max: 100,
step: 1,
},
radius: {
value: 115,
min: 0,
max: 1000,
step: 1,
},
scale: {
value: 100,
min: 0,
max: 1000,
step: 1,
},
preset_: {
value: 'sunset' as const,
options: [
'apartment',
'city',
'dawn',
'forest',
'lobby',
'night',
'park',
'studio',
'sunset',
'warehouse',
] as const,
},
background_: {
options: [false, true, 'only'] as const,
value: true,
},
},
{
collapsed: true,
store: envStore,
}
)
// Memoize the Environment props
const environmentProps = React.useMemo(
() => ({
preset: controls.preset_,
background: controls.background_,
ground: {
height: controls.height,
radius: controls.radius,
scale: controls.scale,
},
}),
[
controls.preset_,
controls.background_,
controls.height,
controls.radius,
controls.scale,
]
)
// Only render if visible
if (!isVisible) return null
return <Environment {...environmentProps} />
})
// Ensure proper display name for debugging
Env.displayName = 'Environment'
export default Env
Video explanation