Distorted (grainy) material due to a normal map?

Hi all!

I have two normal maps, one texture, and one material that I test. When I apply one of the normal maps to a material, I get a weird grainy distorted look on the material. If I replace the normal map with another one, the problem seems to fix itself (although I am not entirely sure whether it is solved or just not pronounced as much). The light dispersion is also weird.

I tried setting receiveShadow and castShadow to false, tried playing with shadow biases, but to no result. Also, I tried setting the minFilter and magFilter, as well as generating mipmaps - no effect.

React-three-fiber code below:

  const ruberoid1 = useMemo(() => {
    ruberoidTexture.colorSpace = SRGBColorSpace;
    // ruberoidTexture.minFilter = NearestMipMapNearestFilter;
    // ruberoidTexture.magFilter = LinearFilter;
    // ruberoidTexture.wrapS = ruberoidTexture.wrapT = RepeatWrapping;
    // ruberoidTexture.generateMipmaps = true;

    // ruberoidNormalMap1.minFilter = NearestMipMapNearestFilter;
    // ruberoidNormalMap1.magFilter = LinearFilter;
    // ruberoidNormalMap1.wrapS = ruberoidNormalMap1.wrapT = RepeatWrapping;
    // ruberoidNormalMap1.generateMipmaps = true;
    // ruberoidNormalMap1.needsUpdate = true;


    const material = new MeshStandardMaterial({
      map: ruberoidTexture,
      normalMap: ruberoidNormalMap2,
      normalScale: new Vector2(2, 2),
      metalness: 1.7,
      roughness: 0.7,
    });

    // ruberoidTexture.needsUpdate = true;
    // ruberoidNormalMap1.needsUpdate = true;

    return material;
  }, [ruberoidNormalMap2, ruberoidTexture]);

The consumer component:

interface RuberoidRoofProps {
  width: number;
  depth: number;
  geometry?: BufferGeometry;
  material?: Material;
  overhangOuter: number;
}

export function RuberoidRoof({
  width,
  depth,
  geometry,
  material,
  overhangOuter,
}: RuberoidRoofProps) {
  const scale = new Vector3(
    width + overhangOuter * 2,
    1,
    depth + overhangOuter * 2
  );

  const adjustedGeometry = useUvAdjustedGeometry({ geometry, scale });
  const offset = 0.012; 

  return (
    <mesh
      scale={scale}
      position={new Vector3(0, 2.2 + 0.32 + offset, 0)}
      geometry={adjustedGeometry}
      material={material}
    />
  );
}

useUvAdjustedGeometry:


interface AdjustGeometryProps {
  geometry?: BufferGeometry;
  scale: Vector3;
  scaleFactor?: number;
}

export function useUvAdjustedGeometry({
  geometry,
  scale,
  scaleFactor = 1,
}: AdjustGeometryProps) {
  return useMemo(() => {
    if (!geometry) return;

    const clonedGeometry = geometry.clone();
    const uvAttribute = clonedGeometry.attributes.uv;
    const uvs = uvAttribute.array;

    for (let i = 0; i < uvs.length; i += 2) {
      uvs[i] *= scale.z * scaleFactor;
      uvs[i + 1] *= scale.x * scaleFactor;
    }

    uvAttribute.needsUpdate = true;
    return clonedGeometry;
  }, [geometry, scale.x, scale.z, scaleFactor]);
}

You can find the texture and normal maps attached.



Links to some videos to visualise the problem:
Link 1
Link 2
Link 3
Link 4

P.S.
Forgot to add info about my lighting.
This is the only lighting I have in the scene:

function Skybox() {
  return (
    <>
      <Environment
        preset="sunset"
        environmentIntensity={0.9}
        background
        backgroundBlurriness={1}
      />
    </>
  );
}

Have you tried LinearMipMapLinear instead of NearestMipMapNearestFilter on the maps, with generateMipmaps enabled?

ruberoidTexture.colorSpace = SRGBColorSpace;

ruberoidNormalMap2.colorSpace = THREE.NoColorSpace

Doesn’t seem to do the trick.

I created a minimal reproducible example where I managed to solve (don’t know how, really) the light reflection issue, ie the arc-like unnatural reflection that can be seen on the screenshots and the video. However, the “grain” is still there, especially when panning the camera.

The result I am trying to reproduce is here:
https://3dvisual-studio.com/canopy/index.html

Furthermore, my material seems to tank performance and I don’t really know why exactly. The configurator at the link above seems to (a) have no grain on the texture; and (b) show decent performance.

Any reason you have metalness set to 1.7 ?

How much the material is like a metal. Non-metallic materials such as wood or stone use 0.0 , metallic use 1.0 , with nothing (usually) in between. Default is 0.0 . A value between 0.0 and 1.0 could be used for a rusty metal look. If metalnessMap is also provided, both values are multiplied.

Regardless, it’s not obvious to me what the problem you are having is. The example you linked looks pretty similar to the codepen to me, but I have old eyeballs…

I do see some moire pattern when rotating, but also your texture looks like it has many repetitions, so it may be being viewed at a non-optimal scale.

I normally work in vanilla, so I don’t know where you specify the renderer settings, such as devicePixelRatio, and antialias etc, so it’s possible there is something non-optimal in the render setup as well.

Metalness of 1.7 gives me a solid approximation of the material used in the example I referenced in the previous message. It approximates the reflection, roughness, and the levels of black. It was the closest result I could achieve. A lower value of metalness gives me more greys instead of black. Basically, it was done by trial and error without due regard for best practices.

Maybe I’m doing it wrong and I’d be happy to do it the right way, if anyone has any suggestions.

The antialias is enabled. I tried setting the pixelRatio to 2, but the grain is still there.

function CanvasContainer({ children, ...rest }: CanvasContainerProps) {
  return (
    <Canvas
      gl={{ antialias: true, pixelRatio: 2 }}
      shadows={true}
      camera={{ position: [7, 6, 8], fov: 60, near: 0.1, far: 1000 }}
      {...rest}
    >
      {children}
      <OrbitControls enablePan={true} enableZoom={true} target={[0, 0, 0]} />
      <Perf position="top-right" />
    </Canvas>
  );
}

I guess, if it looks fine to you and other people here, I’ll probably continue with what I have at the moment, if no solution is found. Thanks for helping, by the way.

1 Like

It also looks like your texture coordinates are stretched on one axis, so that’s also going to create issues with normal maps.
I see that call to “adjustedUV” … you can control repeat of texture via texture.repeat and texture.offset instead of modifying the geometry itself.

1 Like

From the docs on StandardMaterial:

Non-metallic materials such as wood or stone use 0.0 , metallic use 1.0 , with nothing (usually) in between.

See the stretching there?

1 Like

In my local project, the width and depth are updated from a slider. I kinda tried using repeat.set(...), but in my case, locally, the texture just stretches and doesn’t repeat itself. Maybe I am missing something, I will try to update the codesandbox later to see if the issue reproduces there.