如何在Vertex Shader中旋转UV并缩放填充边界且无纹理扭曲?
解决顶点着色器中UV旋转缩放后的纹理扭曲问题
问题分析
当前实现直接在原始UV空间(因平面宽高比不为1,UV的x/y单位长度不等)执行旋转操作,导致旋转后的缩放计算出现偏差,引发纹理扭曲。需先将UV转换到正方形归一化空间完成旋转,再还原并计算正确的填充缩放。
修正方案
核心修改聚焦于rotateAndScaleUV函数:
- 先根据平面宽高比将UV拉伸为正方形空间,消除宽高比对旋转的影响
- 在正方形空间内完成旋转操作
- 将旋转后的UV缩回到原始宽高比空间
- 重新计算填充缩放因子,确保纹理完全填充平面边界框
修正后的完整代码
JavaScript代码
const VERTEX_SHADER = (` varying vec2 vUv; uniform vec2 planeAspect; // 平面的宽高比(width/height) uniform float rotation; // 旋转角度(弧度) vec2 rotateAndScaleUV(vec2 uv, float angle, vec2 planeAspect) { vec2 center = vec2(0.5); // 步骤1:将UV转换到正方形归一化空间(消除宽高比影响) vec2 uvSquare = uv - center; uvSquare.x *= planeAspect.x; // 拉伸x轴到与y轴等长 uvSquare += center; // 步骤2:移动到原点并执行旋转 vec2 uvOrigin = uvSquare - center; float cosA = cos(angle); float sinA = sin(angle); mat2 rotMat = mat2(cosA, -sinA, sinA, cosA); vec2 rotatedUvSquare = rotMat * uvOrigin; // 步骤3:将旋转后的UV还原到原始宽高比空间 vec2 rotatedUv = rotatedUvSquare + center; rotatedUv.x /= planeAspect.x; // 步骤4:计算填充缩放因子 float scale = 1.0 / max(abs(cosA) + abs(sinA) * planeAspect.x, abs(sinA) / planeAspect.x + abs(cosA)); rotatedUv = (rotatedUv - center) * scale + center; return rotatedUv; } void main() { vUv = rotateAndScaleUV(uv, rotation, planeAspect); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `); // 场景初始化 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); // 加载纹理并创建平面 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 planeAspect = [img.width / img.height, 1]; // 平面的宽高比(x/y) // 创建与图片同宽高比的平面几何体 const geometry = new THREE.PlaneGeometry(planeAspect[0], planeAspect[1]); // 着色器材质 const shaderMaterial = new THREE.ShaderMaterial({ uniforms: { textureMap: { value: texture }, planeAspect: { value: planeAspect }, rotation: { value: 0 } }, vertexShader: VERTEX_SHADER, fragmentShader: ` uniform sampler2D textureMap; varying vec2 vUv; void main() { gl_FragColor = texture2D(textureMap, vUv); } ` }); camera.position.z = 1; // 创建网格并添加到场景 const mesh = new THREE.Mesh(geometry, shaderMaterial); scene.add(mesh); // 旋转控制 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); });
CSS代码
body {margin: 0; color: grey;} #container { width: 100vw; height: 100vh; } #ui { position: absolute; top: 5%; left: 50%; transform: translateX(-50%); z-index: 10; }
HTML代码
<script src="https://cdn.jsdelivr.net/npm/three-js@79.0.0/three.min.js"></script> <div id="container"></div> <div id="ui"> <label for="rotation">旋转角度:</label> <input type="range" id="rotation" min="-1" max="1" step="0.001" value="0"> </div>
关键修改说明
- 替换uniform变量:用
planeAspect(平面的宽高比)替代原tSize,直接传递平面宽高比更高效,无需重复计算纹理尺寸比例 - UV正方形空间转换:旋转前将UV的x轴按平面宽高比拉伸,让UV空间变为正方形,确保旋转操作在等比例空间中执行,从根源避免因UV空间非正方形导致的纹理扭曲
- 修正缩放因子计算:基于平面宽高比重新推导填充缩放公式,保证旋转后的纹理始终完全填满平面的边界框
内容的提问来源于stack exchange,提问作者RoyNeary
相关产品推荐
相关产品推荐

