skip to Main Content

I need to rotate and scale UVs in a vertex shader such that the rotated texture fills its available bounding box. The following test implementation successfully rotates and auto-scales the texture but the image gets skewed / distorted as the rotation value increases.

I’m accounting for the texture’s aspect ratio for auto-scaling but I’m definitely missing something in the rotation step.

This question seems related but I’m unable to translate the proposed solution to my vertex shader because I don’t know how Three.js works under the hood.

Any help is greatly appreciated!

const VERTEX_SHADER = (`
  varying vec2 vUv;
  uniform vec2 tSize; // Texture size (width, height)
  uniform float rotation; // Rotation angle in radians

  vec2 rotateAndScaleUV(vec2 uv, float angle, vec2 tSize) {

    vec2 center = vec2(0.5);

    // Step 1: Move UVs to origin for rotation
    vec2 uvOrigin = uv - center;

    // Step 2: Apply rotation matrix
    float cosA = cos(rotation);
    float sinA = sin(rotation);
    mat2 rotMat = mat2(cosA, -sinA, sinA, cosA);
    vec2 rotatedUv = rotMat * uvOrigin;

    // Step 3: Auto-scale to fill available space
    float aspectRatio = tSize.x / tSize.y;
    float scale = 1.0 / max(abs(cosA) + abs(sinA) / aspectRatio, abs(sinA) + abs(cosA) * aspectRatio);
    return rotatedUv * scale + center; // Scale and move back to correct position

  }

  void main() {
    vUv = rotateAndScaleUV(uv, rotation, tSize);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`);

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

// Load an image and create a mesh that matches its aspect ratio
new THREE.TextureLoader().load('https://images.unsplash.com/photo-1551893478-d726eaf0442c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3MDcyNDI0MTB8&ixlib=rb-4.0.3&q=80&w=400', texture => {
  
  texture.minFilter = THREE.LinearFilter;
  texture.generateMipMaps = false;
  
  const img = texture.image;
  const aspectRatio = img.width / img.height;
  
  // Create geometry with the same aspect ratio
  const geometry = new THREE.PlaneGeometry(aspectRatio, 1);
  
  // Shader material
  const shaderMaterial = new THREE.ShaderMaterial({
    uniforms: {
      textureMap: { value: texture },
      tSize: { value: [img.width, img.height] },
      rotation: { value: 0 }
    },
    vertexShader: VERTEX_SHADER,
    fragmentShader: `
      uniform sampler2D textureMap;
      varying vec2 vUv;
      void main() {
        gl_FragColor = texture2D(textureMap, vUv);
      }
    `
  });
  
  camera.position.z = 1;

  // Create and add mesh to the scene
  const mesh = new THREE.Mesh(geometry, shaderMaterial);
  scene.add(mesh);
  
  // UI controls
  document.getElementById('rotation').addEventListener('input', e => {
    shaderMaterial.uniforms.rotation.value = parseFloat(e.target.value);
    renderer.render(scene, camera);
  });
  
  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.render(scene, camera);
  }, false);
  
  renderer.render(scene, camera);
  
});
body {margin: 0; color: grey;}
#container {
  width: 100vw;
  height: 100vh;
}
#ui {
  position: absolute;
  top: 5%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/three.min.js"></script>
<div id="container"></div>
<div id="ui">
  <label for="rotation">Rotation:</label>
  <input type="range" id="rotation" min="-1" max="1" step="0.001" value="0">
</div>

2

Answers


  1. I’ve looked into this a bit, and it seems that to avoid distortion when manipulating the texture you need to adjust the texture scale. I experimented a bit with vertex and managed to remove the distortion when rotating, but it was at the expense of scaling. As you can see, when rotating, the last pixels are stretched. This can be masked a bit, especially in patterns, but it’s a pretty cheap solution. Generally, you only wanted to remove distortions when rotating…

    BTW r79 is quite old version…

    const VERTEX_SHADER = (`
      varying vec2 vUv;
      uniform vec2 tSize;
      uniform float rotation;
      uniform float scaleValue;
    
      vec2 rotateAndScaleUV(vec2 uv, float angle, vec2 tSize) {
        vec2 center = vec2(0.5);
        vec2 uvOrigin = uv - center;
    
        float cosA = cos(angle);
        float sinA = sin(angle);
        mat2 rotMat = mat2(cosA, -sinA, sinA, cosA);
        vec2 rotatedUv = rotMat * uvOrigin;
    
        vec2 scaling = vec2(1.0);
        float aspectRatio = tSize.x / tSize.y;
        if (aspectRatio > 1.0) {
          scaling.x = 1.0 / aspectRatio;
        } else {
          scaling.y = aspectRatio;
        }
        rotatedUv *= scaling * scaleValue;
        rotatedUv += center;
        return rotatedUv;
      }
    
      void main() {
        vec2 centeredUV = position.xy + vec2(0.5);
        vUv = rotateAndScaleUV(centeredUV, rotation, tSize);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `);
    
    // Scene setup
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.getElementById('container').appendChild(renderer.domElement);
    
    // Load an image and create a mesh that matches its aspect ratio
    new THREE.TextureLoader().load('https://images.unsplash.com/photo-1551893478-d726eaf0442c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3MDcyNDI0MTB8&ixlib=rb-4.0.3&q=80&w=400', texture => {
      
      texture.minFilter = THREE.LinearFilter;
      texture.generateMipMaps = false;
      // Like you see, when you commented wrapping, last pixels are streched. You can make this improvement, but is quite... cheap, and decent for  visually impaired people... and seriously, it may only work a little in the patterns.
      texture.wrapS = THREE.RepeatWrapping;
      texture.wrapT = THREE.RepeatWrapping;
     
      const img = texture.image;
      const aspectRatio = img.width / img.height;
      
      // Create geometry with the same aspect ratio
      const geometry = new THREE.PlaneGeometry(aspectRatio, 1);
      
      // Shader material
      const shaderMaterial = new THREE.ShaderMaterial({
      uniforms: {
        textureMap: { value: texture },
        tSize: { value: [img.width, img.height] },
        rotation: { value: 0 },
        scaleValue: { value: 1 },
      },
      vertexShader: VERTEX_SHADER,
      fragmentShader: `
        uniform sampler2D textureMap;
      varying vec2 vUv;
      uniform float scaleValue; // Add scaleValue uniform
    
      void main() {
        vec2 scaledUV = vUv * scaleValue; // Scale UV coordinates
        gl_FragColor = texture2D(textureMap, scaledUV);
      }
      `
    });
      
      camera.position.z = 1;
    
      // Create and add mesh to the scene
      const mesh = new THREE.Mesh(geometry, shaderMaterial);
      scene.add(mesh);
      
      // UI controls
      document.getElementById('rotation').addEventListener('input', e => {
        shaderMaterial.uniforms.rotation.value = parseFloat(e.target.value);
        renderer.render(scene, camera);
      });
      
      document.getElementById('scale').addEventListener('input', e => {
      shaderMaterial.uniforms.scaleValue.value = parseFloat(e.target.value);
      renderer.render(scene, camera);
    });
      
      window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.render(scene, camera);
      }, false);
      renderer.render(scene, camera);
    });
    body {margin: 0; color: grey;}
    #container {
      width: 100vw;
      height: 100vh;
    }
    #ui {
      position: absolute;
      top: 2%;
      left: 50%;
      transform: translateX(-50%);
      z-index: 10;
      display: flex;
      justify-content: center;
    }
    #ui label {
      margin-right:
    }
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/three.min.js"></script>
    <div id="container"></div>
    <div id="ui">
      <label for="rotation">Rotation:</label>
      <input type="range" id="rotation" min="-1" max="1" step="0.001" value="0">
      <label for="scale">Scale:</label>
      <input type="range" id="scale" min="0.1" max="2" step="0.01" value="1">
    </div>
    Login or Signup to reply.
  2. What is off:

    • Once you center in, you need to center out;
    • You scale is kind of off cause of MAD, multiplication then addition, you multiply by scale from the origin 0, 0 and add then center, you need to center before scale.
    const VERTEX_SHADER = (`
      varying vec2 vUv;
      uniform vec2 tSize; // Texture size (width, height)
      uniform float rotation; // Rotation angle in radians
    
      vec2 rotateAndScaleUV(vec2 uv, float angle, vec2 tSize) {
    
        vec2 p = uv;
        float mid = 0.5;    
        float aspect = tSize.x / tSize.y;
        
        float cosA = cos(rotation);
        float sinA = sin(rotation);
        float scale = 1.0 / max(abs(cosA) + abs(sinA) / aspect, abs(sinA) + abs(cosA) * aspect);
        
        mat2 rotMat = mat2(cosA, -sinA, sinA, cosA);
    
        p -= vec2(mid);
        p *= scale;
        p.y *= 1.0 / aspect;
        p *= rotMat;
        
        p.y *= aspect;
        p += vec2(mid);
    
        return p;
    
      }
    
      void main() {
        vUv = rotateAndScaleUV(uv, rotation, tSize);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `);
    
    // Scene setup
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.getElementById('container').appendChild(renderer.domElement);
    
    // Load an image and create a mesh that matches its aspect ratio
    new THREE.TextureLoader().load('https://images.unsplash.com/photo-1551893478-d726eaf0442c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3MDcyNDI0MTB8&ixlib=rb-4.0.3&q=80&w=400', texture => {
      
      texture.minFilter = THREE.LinearFilter;
      texture.generateMipMaps = false;
      
      const img = texture.image;
      const aspectRatio = img.width / img.height;
      
      // Create geometry with the same aspect ratio
      const geometry = new THREE.PlaneGeometry(aspectRatio, 1);
      
      // Shader material
    console.log(img.width, img.height);
      const shaderMaterial = new THREE.ShaderMaterial({
      
        uniforms: {
          textureMap: { value: texture },
          tSize: { value: [img.width, img.height] },
          rotation: { value: 0 }
        },
        vertexShader: VERTEX_SHADER,
        fragmentShader: `
          uniform sampler2D textureMap;
          varying vec2 vUv;
          void main() {
            gl_FragColor = texture2D(textureMap, vUv);
          }
        `
      });
      
      camera.position.z = 1;
    
      // Create and add mesh to the scene
      const mesh = new THREE.Mesh(geometry, shaderMaterial);
      scene.add(mesh);
      
      // UI controls
      document.getElementById('rotation').addEventListener('input', e => {
        shaderMaterial.uniforms.rotation.value = parseFloat(e.target.value);
        renderer.render(scene, camera);
      });
      
      window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.render(scene, camera);
      }, false);
      
      renderer.render(scene, camera);
      
    });
    body {margin: 0; color: grey;}
    #container {
      width: 100vw;
      height: 100vh;
    }
    #ui {
      position: absolute;
      top: 5%;
      left: 50%;
      transform: translateX(-50%);
      z-index: 10;
    }
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/three.min.js"></script>
    <div id="container"></div>
    <div id="ui">
      <label for="rotation">Rotation:</label>
      <input type="range" id="rotation" min="-1" max="1" step="0.001" value="0">
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search