Use the same texture with different offsets on different materials

Hi, I’m using react-three-fiber to create a component, which takes in some parameters, and applies a texture with different UV offsets depending on them. My problem is that, whenever I try to create multiple of these components, the offset of the LAST component I create applies to all of them. After asking around, it turned out that textures have a shared GUID based on the url they were loaded from. Adding a string query to the useLoader statement made it unable to load the texture. Is there a better way to handle a situation like this?

What you could do in such situation (assuming it’s related to this) is:

  1. Load texture image once. Create new Texture instance for each letter, with different offset, and then use these textures in materials for specific meshes (mind this is quite a bruteforce approach and you’ll end up with a lot of textures.)
  2. Create just a single Texture. Apply texture to your meshes. Modify each mesh UV attribute to display the part of the texture you’d like it to display (see mesh.geometry.attributes.uv, this thread may also help.) It’s pretty similar to offset on the texture, but instead of modifying the texture, you modify only the specific mesh.

(also, since it’s react-fiber, @drcmda , maybe there are easier ways there :face_with_monocle:)

1 Like

useLoader is a caching strategy around new Loader().load(url, callback). cache keys are the loader and the url, so calling useLoader with the same cache keys will yield the same texture.

if the query args dont work imo the simplest option would be to do:

function Model({ url }) {
  const texture = useLoader(TextureLoader, url)
  const clone = useMemo(() => texture.clone(), [texture])
  return (
    <mesh>
      <meshBasicMaterial map={clone} />
2 Likes

Hey, thanks for that, but the texture still doesn’t seem to want to load after cloning, it just silently fails without showing anything. This is simplified code if that helps at all.

  const colorTexture: THREE.Texture = useLoader(THREE.TextureLoader, url);

  const clone = useMemo(() => colorTexture.clone(), [colorTexture]);

  clone.wrapS = clone.wrapT = RepeatWrapping;
  clone.repeat.set(
    letterData[2] / 256,
    letterData[3] / 136 + letterData[4] / 136
  );

  clone.offset.x = letterData[0] / 256;
  clone.offset.y = lineYOffsets[getLineNum(props.letter)] - letterData[4] / 136;

  return (
    <a.mesh position-x={props.kerningOffset}>
      <planeBufferGeometry attach="geometry" />
      <meshBasicMaterial map={clone} attach="material" transparent={true} />
    </a.mesh>
  );
};

seeing this makes me think the cached thing was alright, because you’ll be cloning textures for each letter now. the letter shifting should probably be done at the shader level for efficiency. but anyway, if you give me a codesandbox i could at least look into why it fails showing nothing at all, which, assuming your letter shifting code is correct, shouldn’t be. long time ago since i cloned a texture, maybe it was .clone(true) or something trivial like that i hope.

Out of curiosity and sheer ignorance - why not just shift the UVs :thinking: (which will be kinda touching shaders but without actually touching & running additional shaders) ?

https://codesandbox.io/s/r3f-basic-demo-forked-7yv3g there you go. Sorry I’m terrible at making sandboxes but I think this will do, thanks for your help, appreciate it. And also - if cloning is a non-optimal way of handling this, I’m fine with using another approach that mjurczyk suggested.

i think you’re right. i don’t have that much experience with uv’s.

1 Like

so turns out the reason the clone didnt work was bc you forget needsUpdate=true: https://codesandbox.io/s/r3f-basic-demo-forked-ogh9e?file=/src/Letter.js this approach seems wrong to me. better do what @mjurczyk suggested above: a single texture and then you alter geometry.attributes.uv

Sorry for my cluelessness but how would I go about setting the repeat/offset of the uv attributes? Can’t see anything even related to geometry.attributes.uv in the docs…

edit: I found this page which seems helpful, but it doesn’t say anything about the texture.repeat.set alternative, maybe I’m mistaking something here, not sure. https://threejs.org/docs/#api/en/core/BufferAttribute

Texture.repeat.set( x , y ) is equivalent to uv *= vec2( x , y ). It’s just scaling, multiplication.
Texture.offset.set( x , y ) is addition: uv += vec2( x , y ).

So if you want to set “texture repeat” to lets say 3 on both axes, but on geometry level - you would have to get the uv buffer attribute and edit the contents, multiplying each by 3

1 Like

Woo we did it! Thanks for helping everyone!!!
The code if anyone stumbles upon a similar question:

  for (let i = 0; i < uvAttribute.count; i++) {
    let u = uvAttribute.getX(i);
    let v = uvAttribute.getY(i);

    u = (u * letterData[2]) / 256 + letterData[0] / 256;

    v =
      (v * letterData[3]) / 136 +
      letterData[4] / 136 +
      lineYOffsets[getLineNum(props.letter)] -
      letterData[4] / 136;

    uvAttribute.setXY(i, u, v);
  }

2 Likes