pmndrs / drei

🥉 useful helpers for react-three-fiber
https://drei.pmnd.rs/
MIT License
7.83k stars 642 forks source link

Changing Environment File Causing Crash Due to Memory Overflow #2001

Open alexkahndev opened 1 week ago

alexkahndev commented 1 week ago

Problem description:

I am using the Environment component for my scene as the background for my game. I have a customization studio where the user can change the background of their game. This is done by keeping the user settings in state and a URL representing the background chosen. When the user wants to change the background, they click a button that updates the URL part of the state which causes the Environment to reload with the new image. Since the Environment is handling the loading of the image it is also managing and caching its resources. The issue occurs when you change the background a few times which causes the memory to overflow. In my case three backgrounds is usually my consistent limit and I can interchange between them after I've loaded them but loading a fourth causes the crash.

Error Codes

THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false

Material Name: CubemapFromEquirect
Material Type: ShaderMaterial

Program Info Log: 

console.error @ GamePageIndex.1718885885000.js:227
GamePageIndex.1718885885000.js:168  WebGL: INVALID_OPERATION: useProgram: program not valid
x1 @ GamePageIndex.1718885885000.js:168
localhost/:1  WebGL: CONTEXT_LOST_WEBGL: loseContext: context lost
GamePageIndex.1718885885000.js:227 THREE.WebGLRenderer: Context Lost.

Video

https://github.com/pmndrs/drei/assets/140863288/aea7981b-bd4b-4146-a4c4-c0f676049602

Relevant code:

// Background.tsx
import React, { Suspense } from "react";
import { Environment, Html, useProgress } from "@react-three/drei";

type BackgroundProps = {
    backgroundUrl: string;
};

const Loader = () => {
    const { progress } = useProgress();
    return (
        <Html center>
            <div style={{ color: "white" }}>
                Loading... {progress.toFixed(0)}%
            </div>
        </Html>
    );
};

export const Background = ({ backgroundUrl }: BackgroundProps) => {
    return (
        <>
            <Suspense fallback={<Loader />}>
                <Environment background={"only"} files={backgroundUrl} />
            </Suspense>
            <directionalLight
                position={[3.3, 1.0, 4.4]}
                castShadow
                intensity={1}
            />
        </>
    );
};
// BackgroundEditor.tsx
import { CSSProperties, Dispatch, SetStateAction, useState } from "react";
import { ClientSettings } from "../../util/FormTypes";

type BackgroundEditorProps = {
    clientSettings: ClientSettings | null;
    setClientSettings: Dispatch<SetStateAction<ClientSettings | null>>;
};

const backgroundOptions = [
    { label: "Clear Sky", url: "/assets/jpg/sky-background.jpg" },
    { label: "Snowy Mountains", url: "/assets/jpg/mountain-background.jpg" },
    { label: "Outer Space", url: "/assets/jpg/outerspace-background.jpg" },
    { label: "Endless Meadow", url: "/assets/jpg/meadow-background.jpg" }
];

export const BackgroundEditor = ({
    clientSettings,
    setClientSettings
}: BackgroundEditorProps) => {
    if (!clientSettings) {
        return null;
    }

    const [currentSelection, setCurrentSelection] = useState(0);

    const handleSelectionChange = (index: number, url: string) => {
        setCurrentSelection(index);
        setClientSettings((prev) => {
            if (!prev) return null;
            return {
                ...prev,
                background: {
                    ...prev.background,
                    url: url
                }
            };
        });
    };

    const containerStyles: CSSProperties = {
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        backgroundColor: "#f5f5f5",
        padding: "20px",
        boxShadow: "0 0 20px rgba(0, 0, 0, 0.1)"
    };
    const optionsContainerStyles: CSSProperties = {
        display: "flex",
        flexDirection: "column",
        alignItems: "flex-start"
    };

    return (
        <div style={containerStyles}>
            <h1>Background</h1>
            <div style={optionsContainerStyles}>
                {backgroundOptions.map((option, index) => (
                    <label key={option.label}>
                        <input
                            type="radio"
                            name="background"
                            checked={currentSelection === index}
                            onChange={() =>
                                handleSelectionChange(index, option.url)
                            }
                        />
                        {option.label}
                    </label>
                ))}
            </div>
        </div>
    );
};

Suggested solution:

There needs to be a prop for Environment that can control the disposal of the loaded textures. I believe the default should be that on change it disposes of the last texture and then loads the new one. The texture created from environment is very resource heavy. Therefore, users could still override this behavior by setting the prop and cache the other loaded environments. Without the default users having to know about this prop to avoid potential memory overflow.