Environment preset rerender

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

I am not really a React person but you could check the first related topic below, marked as <Environment> from @react-three/drei causes a rerender to see if that could provide any clue to resolving your issue.

1 Like

Thanks a lot for your response, i just did’t notice that detail while i was reading docs.So it must be that way :+1: