pmndrs / react-three-fiber

🇨🇭 A React renderer for Three.js
https://docs.pmnd.rs/react-three-fiber
MIT License
27.38k stars 1.57k forks source link

Texture prop not updating for material #2839

Closed evshiron closed 1 year ago

evshiron commented 1 year ago

Greetings.

I am new to react-three-fiber, and I have spent hours to figure out why it didn't work but without luck.

Here is the demo:

https://codesandbox.io/s/nice-hopper-353nsf?file=/src/App.js

{/* we can confirm the texture works */}
{/* <meshStandardMaterial map={map}></meshStandardMaterial> */}

{/* but if we toggle like this, map won't work */}
{clicked ? (
  <meshStandardMaterial map={map}></meshStandardMaterial>
) : (
  <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'}></meshStandardMaterial>
)}

{/* switching color works fine though */}
{/* {clicked ? (
  <meshStandardMaterial color={'red'}></meshStandardMaterial>
) : (
  <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'}></meshStandardMaterial>
)} */}

{/* what's the proper way to replace/update a material? */}

My real scenario is to add an attached displacement map node to the standard material when it is ready. React did call the render function, but the scene didn't reflect the change even if I replaced the whole material. It works if I remove the condition, but will produce warnings every frame before it is ready.

Any help is appreciated. Thanks in advance.

hutajoullach commented 1 year ago

Looks like ternary doesn't work with conditionally applying texture. You could instead render like this.

{clicked && (
  <meshStandardMaterial map={map}></meshStandardMaterial>
)}

{!clicked && (
  <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'}></meshStandardMaterial>
)}

It might be that when state changes, it's remounting the mesh without textures.

CodyJasonBennett commented 1 year ago

This sounds like a bug in React, mildly related to #2844. I'll see what R3F can do as it's ultimately responsible for the update.

ashleymvanduzer commented 1 year ago

@CodyJasonBennett @evshiron I was able to get it working by wrapping the material with the map prop in a Suspense, I think it just needs time to load / apply to the material to the mesh

Screen Shot 2023-05-18 at 3 26 32 PM
drcmda commented 1 year ago

hmm, i don't think this is a bug ... @CodyJasonBennett

{clicked ? (
  <meshStandardMaterial map={map} />
) : (
  <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
)}

is basically one and the same material in reacts eyes, it creates it once and then just exchanges props when the click condition changes. the culprit is a limitation in threejs, you cannot change a texture after the material has been created without setting needsUpdate, see https://threejs.org/docs/#manual/en/introduction/How-to-update-things

presence or not of

  • texture
  • ... Changes in these require building of new shader program. You'll need to set

material.needsUpdate = true

drcmda commented 1 year ago

the solution @evshiron @ashleymvanduzer

useEffect(() => {
  ref.current.material.needsUpdate = true
}, [clicked])

ps the reason why it works in @ashleymvanduzer's example is because now it's not the same node and react will mount and unmount it, which imo is wasteful. better retain the single material and just call needsupdate, which indeed is a side effect.

i would also write it like this from the get go:

<meshStandardMaterial map={clicked && map} color={clicked ? 'white' : hovered ? 'hotpink' : 'orange'} />

makes it much clearer that we're operating on a single material.

ashleymvanduzer commented 1 year ago

@drcmda nice, I was thinking that would work too, and thanks for the context above, that's actually super good to know

CodyJasonBennett commented 1 year ago

I'm afraid there's nothing R3F can do to help here regardless. I don't see the need of three.js for explicitly flagging materials either, they aren't containers and their state should already be cached. I can look into this upstream, but in the meantime you'll have to work around this gotcha like above.

IWSR commented 1 year ago

@drcmda Thanks, the solution works for me. But I have a question, Why can't react-three-fiber automatically flag which material needs to be updated?

ashleymvanduzer commented 1 year ago

@IWSR Sorry to jump in here but If I understand your question correctly, it's because react three/fiber is just using three js primitives as JSX (via react's reconciler), and threejs requires requires this, so we need to handle the needsUpdate declaration the same way when using React, but with react we can allow mutability via a ref, which means we have direct access to the existing node and we don't have to tear down the dom (and then rebuild it) in order to see the update.

IWSR commented 1 year ago

@ashleymvanduzer Thanks for your response and I apologize for my previous response with incorrect grammar. I was confused earlier because I mistakenly treated tags like \<mesh \/> as React components, which led me to wonder why three/fiber couldn't logically flag the material's updates within the component.

ashleymvanduzer commented 1 year ago

@IWSR No need to apologize! I can see how that would be an easy thing to mix up.