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
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…
What is off:
MAD
, multiplication then addition, you multiply by scale from the origin0, 0
and add then center, you need to center before scale.