diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 1fc7414429..77b614b636 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -1,4 +1,8 @@ import { AnimatePresence } from "framer-motion" +// <<<<<<< HEAD +// import type React from "react" +// import { type ReactElement, useCallback, useEffect, useRef, useState } from "react" +// ======= import { SnackbarProvider } from "notistack" import { useCallback, useEffect, useRef, useState } from "react" import MainHUD from "@/components/MainHUD" @@ -14,6 +18,7 @@ import { globalOpenModal } from "./ui/components/GlobalUIControls.ts" import ProgressNotifications from "./ui/components/ProgressNotification.tsx" import SceneOverlay from "./ui/components/SceneOverlay.tsx" import WPILibConnectionStatus from "./ui/components/WPILibConnectionStatus.tsx" +import { applyInitialGraphicsSettings } from "./ui/helpers/GraphicsSettings.ts" import MainMenuModal from "./ui/modals/MainMenuModal.tsx" import { StateProvider } from "./ui/StateProvider.tsx" import { ThemeProvider } from "./ui/ThemeProvider.tsx" @@ -34,6 +39,8 @@ function Synthesis() { const startSingleplayerCallback = () => { World.initWorld() + applyInitialGraphicsSettings() + if (!PreferencesSystem.getGlobalPreference("ReportAnalytics") && !import.meta.env.DEV) { setConsentPopupDisable(false) } diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts index 13b87468c2..023ea10b0e 100644 --- a/fission/src/systems/preferences/PreferenceTypes.ts +++ b/fission/src/systems/preferences/PreferenceTypes.ts @@ -26,6 +26,7 @@ export type GlobalPreferences = { MuteAllSound: boolean SFXVolume: number ShowCenterOfMassIndicators: boolean + GraphicsOptimizationApplied: boolean } export type GlobalPreference = keyof GlobalPreferences @@ -66,6 +67,7 @@ export const defaultGlobalPreferences: GlobalPreferences = { MuteAllSound: false, SFXVolume: 25, ShowCenterOfMassIndicators: false, + GraphicsOptimizationApplied: false, } export type GraphicsPreferences = { diff --git a/fission/src/ui/helpers/GraphicsSettings.ts b/fission/src/ui/helpers/GraphicsSettings.ts new file mode 100644 index 0000000000..aefd987562 --- /dev/null +++ b/fission/src/ui/helpers/GraphicsSettings.ts @@ -0,0 +1,110 @@ +import PreferencesSystem from "@/systems/preferences/PreferencesSystem" +import { globalAddToast } from "@/ui/components/GlobalUIControls" + +export type GraphicsPreset = "Fast" | "Balanced" | "Fancy" + +export const GRAPHICS_PRESETS: Record< + GraphicsPreset, + { + lightIntensity: number + fancyShadows: boolean + maxFar: number + cascades: number + shadowMapSize: number + antiAliasing: boolean + } +> = { + Fast: { + lightIntensity: 3.5, + fancyShadows: false, + maxFar: 20, + cascades: 3, + shadowMapSize: 1024, + antiAliasing: false, + }, + Balanced: { + lightIntensity: 5, + fancyShadows: true, + maxFar: 30, + cascades: 4, + shadowMapSize: 2048, + antiAliasing: false, + }, + Fancy: { + lightIntensity: 8, + fancyShadows: true, + maxFar: 40, + cascades: 6, + shadowMapSize: 8192, + antiAliasing: true, + }, +} + +export const isMobileDevice = (): boolean => { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) +} + +export const isLowEndDevice = (): boolean => { + if (isMobileDevice()) return true + + const memory = + typeof navigator !== "undefined" && "deviceMemory" in navigator + ? (navigator as Navigator & { deviceMemory?: number }).deviceMemory + : undefined + if (memory !== undefined && memory <= 4) return true // 4GB or less RAM + + const cores = navigator.hardwareConcurrency + if (cores && cores <= 2) return true // 2 cores or less + + const userAgent = navigator.userAgent.toLowerCase() + if (userAgent.includes("android") && (userAgent.includes("lite") || userAgent.includes("go"))) return true + + return false +} + +export const autoOptimizeGraphics = (toastType: "long" | "short" | "none" = "none"): GraphicsPreset => { + if (isLowEndDevice()) { + if (toastType === "long") { + globalAddToast?.( + "info", + "Graphics Optimized", + "We've set your graphics to 'Fast' mode for optimal performance on your device. You can change this in Graphics Settings." + ) + } else if (toastType === "short") { + globalAddToast?.("info", "Success", "Auto-optimized graphics to 'Fast'.") + } + return "Fast" + } else { + if (toastType === "long") { + globalAddToast?.( + "info", + "Graphics Optimized", + "We've set your graphics to 'Balanced' mode for optimal performance on your device. You can change this in Graphics Settings." + ) + } else if (toastType === "short") { + globalAddToast?.("info", "Success", "Auto-optimized graphics to 'Balanced'.") + } + return "Balanced" + } +} + +export const applyInitialGraphicsSettings = (): void => { + // Check if graphics optimization has already been applied + const optimizationApplied = PreferencesSystem.getGlobalPreference("GraphicsOptimizationApplied") + + // If optimization hasn't been applied yet and device should use fast mode, apply fast settings + if (!optimizationApplied) { + const presetToApply = autoOptimizeGraphics("long") + const settings = GRAPHICS_PRESETS[presetToApply] + PreferencesSystem.getGraphicsPreferences().lightIntensity = settings.lightIntensity + PreferencesSystem.getGraphicsPreferences().fancyShadows = settings.fancyShadows + PreferencesSystem.getGraphicsPreferences().maxFar = settings.maxFar + PreferencesSystem.getGraphicsPreferences().cascades = settings.cascades + PreferencesSystem.getGraphicsPreferences().shadowMapSize = settings.shadowMapSize + PreferencesSystem.getGraphicsPreferences().antiAliasing = settings.antiAliasing + + // Mark that optimization has been applied + PreferencesSystem.setGlobalPreference("GraphicsOptimizationApplied", true) + PreferencesSystem.savePreferences() + } +} diff --git a/fission/src/ui/modals/configuring/SettingsModal.tsx b/fission/src/ui/modals/configuring/SettingsModal.tsx index 58635714e4..a076a58806 100644 --- a/fission/src/ui/modals/configuring/SettingsModal.tsx +++ b/fission/src/ui/modals/configuring/SettingsModal.tsx @@ -29,8 +29,10 @@ const SettingsModal: React.FC> = ({ modal }) => { SoundPlayer.changeVolume() } - configureScreen(modal!, { title: "Settings", allowClickAway: false }, { onBeforeAccept: save, onCancel }) - }, [modal, save]) + if (modal) { + configureScreen(modal, { title: "Settings", allowClickAway: false }, { onBeforeAccept: save, onCancel }) + } + }, [modal, save, configureScreen]) const writePreference = (pref: K, value: GlobalPreferences[K]) => { PreferencesSystem.setGlobalPreference(pref, value) diff --git a/fission/src/ui/panels/GraphicsSettingsPanel.tsx b/fission/src/ui/panels/GraphicsSettingsPanel.tsx index eb5e9815d0..6be47cd955 100644 --- a/fission/src/ui/panels/GraphicsSettingsPanel.tsx +++ b/fission/src/ui/panels/GraphicsSettingsPanel.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Stack } from "@mui/material" +import { Box, Button, FormControl, InputLabel, MenuItem, Select, Stack } from "@mui/material" import type React from "react" import { useEffect, useState } from "react" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" @@ -7,6 +7,7 @@ import Checkbox from "../components/Checkbox" import Label from "../components/Label" import type { PanelImplProps } from "../components/Panel" import StatefulSlider from "../components/StatefulSlider" +import { autoOptimizeGraphics, GRAPHICS_PRESETS, type GraphicsPreset } from "../helpers/GraphicsSettings" import { useUIContext } from "../helpers/UIProviderHelpers" const MIN_LIGHT_INTENSITY = 1 @@ -20,6 +21,29 @@ const MAX_CASCADES = 8 const MIN_SHADOW_MAP_SIZE = 1024 +const detectCurrentPreset = ( + lightIntensity: number, + fancyShadows: boolean, + maxFar: number, + cascades: number, + shadowMapSize: number, + antiAliasing: boolean +): string => { + for (const [presetName, presetSettings] of Object.entries(GRAPHICS_PRESETS)) { + if ( + presetSettings.lightIntensity === lightIntensity && + presetSettings.fancyShadows === fancyShadows && + presetSettings.maxFar === maxFar && + presetSettings.cascades === cascades && + presetSettings.shadowMapSize === shadowMapSize && + presetSettings.antiAliasing === antiAliasing + ) { + return presetName + } + } + return "Custom" +} + const GraphicsSettingsPanel: React.FC> = ({ panel }) => { const { configureScreen } = useUIContext() const [reload, setReload] = useState(false) @@ -32,7 +56,100 @@ const GraphicsSettingsPanel: React.FC> = ({ panel }) const [shadowMapSize, setShadowMapSize] = useState(PreferencesSystem.getGraphicsPreferences().shadowMapSize) const [antiAliasing, setAntiAliasing] = useState(PreferencesSystem.getGraphicsPreferences().antiAliasing) - // TODO: save preferences on accept, reload if needed + const initialPreset = detectCurrentPreset( + PreferencesSystem.getGraphicsPreferences().lightIntensity, + PreferencesSystem.getGraphicsPreferences().fancyShadows, + PreferencesSystem.getGraphicsPreferences().maxFar, + PreferencesSystem.getGraphicsPreferences().cascades, + PreferencesSystem.getGraphicsPreferences().shadowMapSize, + PreferencesSystem.getGraphicsPreferences().antiAliasing + ) + + const [selectedPreset, setSelectedPreset] = useState(initialPreset) + + const updatePresetFromSettings = ( + newLightIntensity: number, + newFancyShadows: boolean, + newMaxFar: number, + newCascades: number, + newShadowMapSize: number, + newAntiAliasing: boolean + ) => { + const detectedPreset = detectCurrentPreset( + newLightIntensity, + newFancyShadows, + newMaxFar, + newCascades, + newShadowMapSize, + newAntiAliasing + ) + if (detectedPreset !== selectedPreset) { + setSelectedPreset(detectedPreset) + } + } + + const updateCSMSettingsAndPreset = ( + newLightIntensity: number, + newFancyShadows: boolean, + newMaxFar: number, + newCascades: number, + newShadowMapSize: number, + newAntiAliasing: boolean + ) => { + World.sceneRenderer.changeCSMSettings({ + lightIntensity: newLightIntensity, + fancyShadows: newFancyShadows, + maxFar: newMaxFar, + cascades: newCascades, + shadowMapSize: newShadowMapSize, + antiAliasing: newAntiAliasing, + }) + updatePresetFromSettings( + newLightIntensity, + newFancyShadows, + newMaxFar, + newCascades, + newShadowMapSize, + newAntiAliasing + ) + } + + const applyPreset = (preset: GraphicsPreset) => { + const settings = GRAPHICS_PRESETS[preset] + const previousAntiAliasing = antiAliasing + + setLightIntensity(settings.lightIntensity) + setFancyShadows(settings.fancyShadows) + setMaxFar(settings.maxFar) + setCascades(settings.cascades) + setShadowMapSize(settings.shadowMapSize) + setAntiAliasing(settings.antiAliasing) + setSelectedPreset(preset) + + // Apply settings to scene renderer immediately + World.sceneRenderer.changeLighting(settings.fancyShadows) + World.sceneRenderer.setLightIntensity(settings.lightIntensity) + + World.sceneRenderer.changeCSMSettings({ + lightIntensity: settings.lightIntensity, + fancyShadows: settings.fancyShadows, + maxFar: settings.maxFar, + cascades: settings.cascades, + shadowMapSize: settings.shadowMapSize, + antiAliasing: settings.antiAliasing, + }) + + if (previousAntiAliasing !== settings.antiAliasing) { + setReload(true) + } + } + + const handleAntiAliasingChange = (checked: boolean) => { + setAntiAliasing(checked) + setReload(true) + updatePresetFromSettings(lightIntensity, fancyShadows, maxFar, cascades, shadowMapSize, checked) + } + useEffect(() => { const onBeforeAccept = () => { PreferencesSystem.getGraphicsPreferences().fancyShadows = fancyShadows @@ -47,23 +164,72 @@ const GraphicsSettingsPanel: React.FC> = ({ panel }) if (reload) window.location.reload() } const onCancel = () => { - World.sceneRenderer.changeLighting(PreferencesSystem.getGraphicsPreferences().fancyShadows) + // Revert all settings to original preferences + const originalPrefs = PreferencesSystem.getGraphicsPreferences() + World.sceneRenderer.setLightIntensity(originalPrefs.lightIntensity) + World.sceneRenderer.changeLighting(originalPrefs.fancyShadows) + World.sceneRenderer.changeCSMSettings({ + lightIntensity: originalPrefs.lightIntensity, + fancyShadows: originalPrefs.fancyShadows, + maxFar: originalPrefs.maxFar, + cascades: originalPrefs.cascades, + shadowMapSize: originalPrefs.shadowMapSize, + antiAliasing: originalPrefs.antiAliasing, + }) } - configureScreen(panel!, { title: "Graphics Settings", position: "center" }, { onBeforeAccept, onCancel }) - }, [fancyShadows, lightIntensity, maxFar, cascades, shadowMapSize, antiAliasing, reload]) + if (panel) { + configureScreen(panel, { title: "Graphics Settings", position: "center" }, { onBeforeAccept, onCancel }) + } + }, [fancyShadows, lightIntensity, maxFar, cascades, shadowMapSize, antiAliasing, reload, panel, configureScreen]) return ( + + + Preset + + + + + + val.toFixed(2)} onChange={value => { - setLightIntensity(value as number) - World.sceneRenderer.setLightIntensity(value as number) + const newValue = value as number + setLightIntensity(newValue) + World.sceneRenderer.setLightIntensity(newValue) + updatePresetFromSettings(newValue, fancyShadows, maxFar, cascades, shadowMapSize, antiAliasing) }} step={0.25} /> @@ -73,64 +239,69 @@ const GraphicsSettingsPanel: React.FC> = ({ panel }) onClick={checked => { setFancyShadows(checked) World.sceneRenderer.changeLighting(checked) + World.sceneRenderer.setLightIntensity(lightIntensity) + updatePresetFromSettings(lightIntensity, checked, maxFar, cascades, shadowMapSize, antiAliasing) }} /> {fancyShadows && ( <> { - setMaxFar(value as number) - World.sceneRenderer.changeCSMSettings({ - maxFar: value as number, - + const newValue = value as number + setMaxFar(newValue) + updateCSMSettingsAndPreset( lightIntensity, fancyShadows, + newValue, cascades, shadowMapSize, - antiAliasing, - }) + antiAliasing + ) }} step={1} /> { - setCascades(value as number) - World.sceneRenderer.changeCSMSettings({ - cascades: value as number, - - maxFar, + const newValue = value as number + setCascades(newValue) + updateCSMSettingsAndPreset( lightIntensity, fancyShadows, + maxFar, + newValue, shadowMapSize, - antiAliasing, - }) + antiAliasing + ) }} step={1} /> { - setShadowMapSize(value as number) - World.sceneRenderer.changeCSMSettings({ - shadowMapSize: value as number, - - maxFar, + const newValue = value as number + setShadowMapSize(newValue) + updateCSMSettingsAndPreset( lightIntensity, fancyShadows, + maxFar, cascades, - antiAliasing, - }) + newValue, + antiAliasing + ) }} step={1024} /> @@ -143,11 +314,11 @@ const GraphicsSettingsPanel: React.FC> = ({ panel }) setCascades(4) World.sceneRenderer.changeCSMSettings({ - shadowMapSize, - maxFar, - lightIntensity, + shadowMapSize: 4096, + maxFar: 30, + lightIntensity: 5, fancyShadows, - cascades, + cascades: 4, antiAliasing, }) }} @@ -158,14 +329,7 @@ const GraphicsSettingsPanel: React.FC> = ({ panel }) )} - { - setAntiAliasing(checked) - setReload(true) - }} - /> + ) }