3
votes

I'd like to enable a user to rotate a texture on a rectangle while keeping the aspect ratio of the texture image intact. I'm doing the rotation of a 1:1 aspect ratio image on a surface that is rectangular (say width: 2 and length: 1)

Steps to reproduce: In the below texture rotation example

https://threejs.org/examples/?q=rotation#webgl_materials_texture_rotation

If we change one of the faces of the geometry like below:

https://github.com/mrdoob/three.js/blob/master/examples/webgl_materials_texture_rotation.html#L57

var geometry = new THREE.BoxBufferGeometry( 20, 10, 10 );

Then you can see that as you play around with the rotation control, the image aspect ratio is distorted. (form a square to a weird shape)

At 0 degree: At 0 Degree

At some angle between 0 and 90: enter image description here

I understand that by changing the repeatX and repeatY factor I can control this. It's also easy to see what the values would be at 0 degree, 90 degree rotations.

But I'm struggling to come up with the formula for repeatX and repeatY that works for any texture rotation given length and width of the rectangular face.

2
Is this what you need? github.com/mrdoob/three.js/issues/1847gaitat
@gaitat Thanks for suggesting it, I already read that, all that article issue shows is which property to change for editing scale and offset. I already know that it can be done by editing those properties. But the problem is if there's a way to change those properties without distorting the scale of the original image.noob Mama
This looks like a trigonometry problem. If you can set up a JSfiddle or similar I will look into it...Tom
I'm not entirely sure what your finished result should look like. Are you trying to apply rotations while maintaining the wide rectangles without getting the stretched "diamond" look?Marquizzo

2 Answers

3
votes

Unfortunately when stretching geometry like that, you'll get a distortion in 3D space, not UV space. In this example, one UV.x unit occupies twice as much 3D space as one UV.y unit: enter image description here

This is giving you those horizontally-skewed diamonds when in between rotations: enter image description here

Sadly, there's no way to solve this with texture matrix transforms. The horizontal stretching will be applied after the texture transform, in 3D space, so texture.repeat won't help you avoid this. The only way to solve this is by modifying the UVs so the UV.x units take up as much 3D space as UV.y units: enter image description here

With complex models, you'd do this kind of "equalizing" in a 3D editor, but since the geometry is simple enough, we can do it via code. See the example below. I'm using a width/height ratio variable to use in my UV.y remapping, that way the UV transformations will match up, regardless of how much wider it is.

//////// Boilerplate Three setup
const renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
const camera = new THREE.PerspectiveCamera(50, 1, 1, 100);
camera.position.z = 3;
const scene = new THREE.Scene();

/////////////////// CREATE GEOM & MATERIAL
const width = 2;
const height = 1;
const ratio= width / height;  // <- magic number that will help with UV remapping
const geometry = new THREE.BoxBufferGeometry(width, height, width);

let uvY;
const uvArray = geometry.getAttribute("uv").array;

// Re-map UVs to avoid distortion
for (let i2 = 0; i2 < uvArray.length; i2 += 2){
  uvY = uvArray[i2 + 1];  // Extract Y value, 
  uvY -= 0.5;             // center around 0
  uvY /= ratio;           // divide by w/h ratio
  uvY += 0.5;             // remove center around 0
  uvArray[i2 + 1] = uvY;
}
geometry.getAttribute("uv").needsUpdate = true;

const uvMap = new THREE.TextureLoader().load("https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/uv_grid_opengl.jpg");

// Now we can apply texture transformations as expected
uvMap.center.set(0.5, 0.5);
uvMap.repeat.set(0.25, 0.5);
uvMap.anisotropy = 16;

const material = new THREE.MeshBasicMaterial({map: uvMap});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

window.addEventListener("mousemove", onMouseMove);
window.addEventListener("resize", resize);

// Add rotation on mousemove
function onMouseMove(ev) {
	uvMap.rotation = (ev.clientX / window.innerWidth) * Math.PI * 2;
}

function resize() {
	const width = window.innerWidth;
	const height = window.innerHeight;
	renderer.setSize(width, height);
	camera.aspect = width / height;
	camera.updateProjectionMatrix();
}

function animate(time) {
  mesh.rotation.y = Math.cos(time/ 3000) * 2;
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

resize();
requestAnimationFrame(animate);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://threejs.org/build/three.js"></script>
<canvas></canvas>
1
votes

First of all, I agree with the solution @Marquizzo provided to your problem. And setting UV explicitly to the geometry should be the easiest way to solve your problem.

But @Marquizzo did not answer why changing the matrix of the texture (set repeatX and repeatY) does not work.

We all know the 2D rotation matrix R

cos  -sin
sin  cos

UVs are calculated in the shader with a transform matrix T, which is the texture matrix from your question.

T * UV = new UV

To simplify the question, we only consider rotation. And assume we have another additional matrix X for calculating the new UV. Then we have

X * R * UV = new UV

The question now is whether we can find a solution ofX, so that with any rotation, new UV of any points in your question can be calculated correctly. If there is a solution of X, then we can simply use

var X = new Matrix3();
//X.set(x,y,z,...)
texture.matrix.premultiply(X);

Otherwise, we can't find the approach you expected.

Let's create several equations to figure out X.

In the pic below, ABCD is one face of your geometry, and the transparent green is the texture. The UV of point A is (0,1), point B is (0,0), and (1,0), (1,1) for C and D respectively.

The first equation comes from the consideration, without any rotation, the original UV should never be changed (UV for A is always (0,1)). So we should have

X * I * (0, 1) = (0, 1) // I is the identity matrix

From here we can see X should also be an identity matrix.

Then let's see whether the identity matrix X can satisfy the second equation. What's the second equation? Simplify again, let B be the rotation centre(origin) and rotate the texture 90 degrees(counterclockwise). We use -90 to calculate UV though we rotate 90 degrees.

The new UV for point A after rotating the texture 90 degrees should be the current value of E. The value of E is (a/b, 0). Then we have enter image description here From this equation we can see X should not be an identity matrix, which means, WE ARE NOT ABLE TO FIND A SOLUTION OF X TO SOLVE YOUR PROBLEM WITH

X * R * UV = new UV

Certainly, you can change the shader of calculating new UVs, but it's even harder than the way @Marquizzo provided.

enter image description here