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.

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);