I'm working on rotating and scaling UVs in a vertex shader to ensure that the rotated texture completely fills its available bounding box. My current implementation is successfully rotating and auto-scaling the texture, but I'm facing an issue where the image gets skewed or distorted as the rotation value increases.
While I have accounted for the texture's aspect ratio for auto-scaling, there seems to be something missing in the rotation step that I'm unable to figure out.
Although this question seems related to my issue, I am struggling to adapt the proposed solution to my vertex shader due to a lack of understanding of how Three.js operates at a lower level.
I would greatly appreciate any assistance!
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/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="84f0ecf6e1e1a9eef7c4b3bdaab4aab4">[email protected]</a>/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>