How to preload the texture and assets upfront before using it?

Hi, My Question is bit more inclined towards Javascript than ThreeJS but both are related.

I’m making a demo where I dynamically change the texture on glb material based on scroll position. But the problem is whenever I’m changing the texture the screen goes blank for 1-2 seconds as the browser tries to fetch it on the go. Now I seen people using http caching and service worker cache API but I just don’t want to deal with cache invalidation issues and this demo shows that even with both the cache the file is not instantly accessed.

So my question is I’m using React three fiber so how can prefetch all the data upfront in useEffect or useLayoutEffect without caching them?

So far my observation is that once the file is being loaded, the screen doesn’t go blank and the texture can be swapped instantly without any issue and warning in the console. Due to this problem my console is telling me that I’m recalling ktx2loader multiple times when the browser tries to get the data.

1 Like

there is a component for that in drei <Preload all /> this will force everything to be visible to a camera once and flush the textures to gpu, which is especially useful for scroll. by default three will first upload and process an object when it “sees” it eg if it’s in the rendering cameras frustum.

otherwise all loader hooks have preload functions, for instance useTexture.preload(“/foo.png”) in global space. keep in mind that this will not upload to the gpu, only fetch, the upload is the stressful part and should never happen runtime.

also make sure you wrap stuff into suspense. if you don’t everything that is within the same bound of something that loads gets unmounted. there’s also useTransition which retains the current thing until the new is ready, it won’t end up in a fallback.

useTexture.preload("/mountains.png")
useTexture.preload("/cities.png")

function Image({ url }) {
  const texture = useTexture(url)
  ...

function Scene() {
  const [url, setUrl] = useState("/mountains.png")
  ..
  // this is safe
  <SomeModel />
  <Suspense fallback={<AnythingThatShowsUpWhileLoading />}>
    // this will run into the fallback above when loading,
    // but leaves everything else untouched
    <Image url={url} />
  </Suspense>

and with transitions

const [url, setUrl] = useState("/mountains.png")
const [isPending, startTransition] = useTransition()
...
useEffect(() => {
  startTransition(() => setUrl("/cities.png")

// you can show pending state optionally
{isPending && <ComponentThatShowsPendingState />}
// this is safe
<SomeModel />
<Suspense fallback={<ThisWillOnlyShowOnFirstLoad />}>
  // on first load this will run into the fallback above when loading,
  // but leaves everything else untouched. when the url is changed
  // the old image will persist until the new one has loaded
  <Image url={url} />
</Suspense>

I’m making a demo where I dynamically change the texture on glb material based on scroll position

this could be a bad idea. the problem is texture upload, which is an expensive op. you have to look through stack overflow if it’s possible to do that pre-emptively.

Really Thanks for the code snippet.
To give you an brief context, I’m using useKTX2 loader

These are the file paths

// file paths
  const afg_hlg = 'assets/img/nasaBlackMarble/afghanistan_hlg.ktx2';
  const arg_hlg = 'assets/img/nasaBlackMarble/argentina_hlg.ktx2';
  const argRailway_hlg = 'assets/img/nasaBlackMarble/argentinaRailway_hlg.ktx2';

and according to your shoe configurator demo I’m using useState to inject the path in the useKTX2 hook like below

const [topLayer, setTopLayer] = useState(afg_hlg);
const [topLayerOpacity, setTopLayerOpacity] = useState(0);
let [baseLayerTexture, topLayerTexture] = useKTX2([nasaBlackMarble2016, topLayer]);

So my confusion is currently I’m fetching all the path for topLayerTexture with useState which is causing this issue, should I ditch the useState and describe it in upfront as below

useKTX2.preload(afg_hlg);
useKTX2.preload(arg_hlg);
useKTX2.preload(argRailway_hlg);
let [afghanistan, argentina, argentinaRailway, ] = useKTX2([afg_hlg, arg_hlg, argRailway_hlg])

and how do I inject these texture into my mesh with EventListner mentioned below

<mesh
  name="top"
  geometry={nodes.top.geometry}
  // material={materials.top}
>
  <meshStandardMaterial
    map={topLayer}
    map-flipY={false}
    needsUpdate={true}
    transparent={true}
    opacity={topLayerOpacity}
  />
</mesh>;

Update, after thinking it for a while, I finally got the solution

It turns out that I was doing it in a wrong way. I still need useState and the following code works perfectly without any glitch thanks to @drcmda for suggesting me for using useKTX2.preload()

// file paths
  const afg_hlg = 'assets/img/nasaBlackMarble/afghanistan_hlg.ktx2';
  const arg_hlg = 'assets/img/nasaBlackMarble/argentina_hlg.ktx2';
  const argRailway_hlg = 'assets/img/nasaBlackMarble/argentinaRailway_hlg.ktx2';

// Preloading the Textures
useKTX2.preload(afg_hlg);
useKTX2.preload(arg_hlg);
useKTX2.preload(argRailway_hlg);

let [afghanistan, argentina, argentinaRailway ] = useKTX2([afg_hlg, arg_hlg, argRailway_hlg])

const [topLayer, setTopLayer] = useState(afghanistan);

//Any EventListner
document.getElementById('button').addEventListner('click', () => {
setTopLayer(argentina)
})

<mesh
  name="top"
  geometry={nodes.top.geometry}
>
  <meshStandardMaterial
    map={topLayer}
    map-flipY={false}
    needsUpdate={true}
    transparent={true}
    opacity={topLayerOpacity}
  />
</mesh>;

right, useKTX2.preload(afg_hlg) is for global space, right in the module, not the component. this makes sure it starts to load (and parse!) right away. the GPU upload happens when three “sees” it, or using <Preload all /> but that would only work if there are meshes in the scene that do have the textures on them.

1 Like

One more question, can we wrap all this into one preload() function or do we need to explicitly write all of them?

useKTX2.preload(afg_hlg);
useKTX2.preload(arg_hlg);
useKTX2.preload(argRailway_hlg);

Hi drcmda, can you explain to me how i implement the transition between two textures? I’m not really getting the code you are providing. I am familiar with Suspense to set a fallback option. But the effect i want is that the texture stays the same until the other is loaded. I am using zustand to get global variables that control which texture gets loaded. So do i have to save the state before somehow and then change it when the new map is ready? Here is my component i want to use it in:

import { useGLTF, useTexture } from "@react-three/drei";
import { UmgebungStore } from "../../contexts/Stores/UmgebungStore";
import { TürenStore } from "../../contexts/Stores/TürenStore";


export function Boden() {
  const { nodes: nodes } = useGLTF("./models/Umgebung/Böden.glb");

  const BODEN_OFL = UmgebungStore((state) => state.BODEN_OFL);
  const BODEN_TYP = UmgebungStore((state) => state.BODEN_TYP);

  const AFLG = TürenStore((state) => state.AFLG);

  const bodenTexture = useTexture({
    map: `./textures/bodenTexture/${BODEN_TYP}/${BODEN_OFL}/${AFLG}/1.jpg`,
  });

  return (
    <>
      <mesh receiveShadow geometry={nodes.Cube059.geometry}>
        <meshStandardMaterial
          envMapIntensity={0.1}
          map={bodenTexture.map}
          toneMapped={false}
        />
      </mesh>
    </>
  );
}

you can use useTransition or useDeferredValue to flag a component as pending. if it pends it will not go into the suspense fallback but hang on to the current result. you would use transitions if you control state yourself. if something like zustand, redux, leva etc holds state for you, then just defer the value.

const OFL = UmgebungStore((state) => state.BODEN_OFL)
const TYP = UmgebungStore((state) => state.BODEN_TYP)
const AFLG = TürenStore((state) => state.AFLG)
const deferred = useDeferredValue(`./textures/bodenTexture/${TYP}/${OFL}/${AFLG}/1.jpg`)
const bodenTexture = useTexture({ map: deferred })

here’s a small example

it

  • goes into a fallback on first load
  • hangs on to the current result on successive loads
  • you could show pending state by comparing url against deferred

PS, “UmgebungStore” is against convention. hooks should be lowercase and start with “use”. a linter would not let that pass. better rename to useUmgebungStore.

2 Likes

Wow that works great!! Thanks for the solution, exactly what i was looking for :star_struck:

PS: i’ll change that thanks for the tip

well it works for me. But it came with heavy performance issues when deploying it to others with slower maschines. I use this deferred value for about 16 different textures. Is this a problem?

suspense and defer have no relation to that. you have a texture, three renders it. you should generally consider using one and the same material for all these meshes if the texture matches but that’s a purely threejs related concern.

yes i use the same Material for all of them and just deferring the map. But good to know that useDeferred value is not the problem, i read a bit about it now. I’ll have to check my code then to fix this. I come back if i find the issue