three.js实现纹理offset偏移

three.js实现纹理offset偏移

预览

在这里插入图片描述

代码

<template>
  <div class="app">
    <div ref="canvesRef" class="canvas-wrap"></div>
  </div>
</template>

<script setup>
import {
   
    ref, onMounted } from "vue";
import * as THREE from "three";
import {
   
    OrbitControls } from "three/addons/controls/OrbitControls.js";

const canvesRef = ref(null);
const canvasWidth = window.innerWidth;
const canvasHeight = window.innerHeight;
<think>我们正在讨论的是在three.js中如何根据纹理贴图的宽高动态调整repeat和offset参数。 用户的需求是:根据加载的纹理图片的实际尺寸来动态设置纹理的repeat和offset属性。这通常用于当纹理的宽高比不是1:1时,我们希望保持纹理的正确比例,避免拉伸。 在three.js中,纹理对象(THREE.Texture)有repeat和offset属性。repeat是一个二维向量(Vector2),表示在U和V方向上的重复次数;offset也是一个二维向量,表示纹理偏移。 然而,纹理图片的宽高比可能与模型表面的宽高比不一致。为了保持纹理的正确比例,我们可能需要根据图片的宽高比来调整repeat值,使得纹理在模型上正确显示而不变形。 常见做法: 1. 加载纹理图片,并获取其宽度和高度。 2. 计算纹理的宽高比(aspect = width/height)。 3. 根据模型表面的宽高比(或者我们期望的显示区域宽高比)和纹理宽高比,来调整repeat值。 但注意:用户的问题中并没有指定模型表面的宽高比。所以这里可能需要分两种情况: a) 如果用户希望纹理按照原始比例重复铺满整个模型(比如一个平面),那么我们需要根据模型尺寸和纹理尺寸来计算repeat。 b) 或者,用户只是希望纹理在保持原始比例的情况下显示一次(不重复),那么我们可以通过设置repeat为(1,1)并调整offset来居中,但这样可能会有空白。或者通过调整模型的UV。 不过,用户的问题描述是“根据纹理贴图的宽高动态调整repeat和offset”,所以我们主要关注如何根据图片尺寸来设置这两个属性。 实际上,如果我们希望纹理在模型上重复显示,并且保持纹理的原始比例,那么我们需要知道模型表面的宽高比(比如一个平面几何体的宽高)。然后,我们可以通过以下方式计算: repeatU = 模型宽度 / (纹理宽度 * 期望的纹理显示宽度比例因子) // 这里可能需要根据具体需求调整 repeatV = 模型高度 / (纹理高度 * 期望的纹理显示高度比例因子) 但这可能比较复杂。另一种常见的需求是:让纹理在保持宽高比的情况下覆盖整个平面,类似于CSS中的`background-size: contain`或`cover`。 在three.js实现类似`contain`的效果(即保持比例,完整显示纹理,可能有空白): - 计算纹理宽高比:aspect = image.width / image.height - 计算模型(平面)宽高比:planeAspect = planeWidth / planeHeight - 比较aspect和planeAspect: 如果aspect > planeAspect,说明纹理相对于平面更宽,那么我们需要在V方向重复更多次(或者U方向重复更少)来保持比例。具体: repeat.set(1, planeAspect / aspect); 如果aspect < planeAspect,说明纹理更高,那么: repeat.set(aspect / planeAspect, 1); 但是注意:这样设置repeat会导致纹理重复的次数小于1(即只显示部分),所以通常我们不会这样设置repeat,而是调整UV映射。不过,用户要求使用repeat和offset。 另一种思路:我们设置repeat为(1,1)然后通过调整offset和repeat来达到保持比例且填满的效果(类似cover)?但这样会裁剪纹理。 由于问题描述不够具体,我们假设用户希望纹理按照原始比例重复覆盖整个平面,并且不拉伸(即保持比例,重复铺满,类似于墙纸)。那么我们可以这样: repeatU = 平面宽度 / 纹理宽度; repeatV = 平面高度 / 纹理高度; 这样设置后,纹理就会按照原始大小在平面上重复。但是,这样设置会导致纹理的重复次数与平面尺寸成正比,即平面越大,重复次数越多。这通常是我们想要的(比如一个10x10的平面,纹理是512x512,那么重复次数就是10/(512/单位长度)?注意单位问题)。 然而,在three.js中,纹理的重复默认是1,即不重复。如果我们设置repeatU和repeatV为上面的计算值,那么纹理就会重复多次。 但是,用户可能希望的是纹理在平面上只显示一次,并且保持比例(不拉伸)。那么我们可以这样: 1. 设置repeat为(1,1) 2. 计算纹理的宽高比和平面宽高比 3. 为了保持纹理比例,我们需要调整UV,但用户要求使用offset和repeat,所以我们可以通过改变repeat和offset实现类似“视口”的效果。 实际上,如果用户希望纹理保持比例并居中显示(不重复),我们可以通过以下步骤: - 计算纹理宽高比:textureAspect = image.width / image.height - 计算平面宽高比:planeAspect = plane.width / plane.height - 如果textureAspect > planeAspect,说明纹理更宽,那么我们需要将纹理在高度方向上完全显示,宽度方向按比例显示(即高度方向占满,宽度方向两侧超出)。那么: repeat.set(planeAspect / textureAspect, 1) offset.set((1 - planeAspect/textureAspect)/2, 0) - 如果textureAspect < planeAspect,说明纹理更高,那么: repeat.set(1, textureAspect / planeAspect) offset.set(0, (1 - textureAspect/planeAspect)/2) 这样,我们就将纹理按比例缩放并居中显示在平面上了。注意,这样设置后,纹理不会重复,因为repeat的每个分量都小于或等于1,并且我们通过offset调整了位置。 但是,用户的问题中要求“动态调整repeat和offset”,所以我们需要在纹理加载完成后进行计算。 实现步骤: 1. 加载纹理,在加载完成后获取图片的尺寸。 2. 获取需要应用纹理的模型的尺寸(这里假设是一个平面,我们可以通过几何体的bounding box计算宽度和高度)。 3. 根据上述公式计算repeat和offset。 4. 设置纹理的repeat和offset,并设置wrapS和wrapT为THREE.ClampToEdgeWrapping(避免重复)?或者根据需求,如果希望重复则设置为THREE.RepeatWrapping。 注意:如果用户希望的是重复纹理并保持比例,那么上面的第一种方法(直接根据平面尺寸和纹理尺寸计算repeat)更合适。 由于用户的问题没有明确是“contain”还是“cover”还是“repeat”,我们将提供两种常见情况的代码示例: 情况1:保持纹理比例,重复铺满整个平面(类似于墙纸,重复纹理) ```javascript const textureLoader = new THREE.TextureLoader(); textureLoader.load('texture.jpg', function(texture) { // 获取图片尺寸 const image = texture.image; const imageWidth = image.width; const imageHeight = image.height; // 假设我们有一个平面几何体,其宽度和高度已知(例如10和5) const planeWidth = 10; const planeHeight = 5; // 计算重复次数:平面尺寸除以纹理尺寸(注意:这里假设纹理的每个像素对应一个单位?实际上,在three.js中,纹理坐标是0~1,所以我们需要将平面尺寸除以纹理尺寸(但纹理尺寸本身没有单位,我们需要考虑纹理在模型上的缩放) // 实际上,在three.js中,纹理的重复是基于UV坐标的。默认情况下,UV坐标0~1对应整个平面。所以如果我们设置repeat为(planeWidth, planeHeight),那么纹理就会重复planeWidth次在U方向,planeHeight次在V方向。 // 但是,这样设置会导致纹理被拉伸?因为平面UV是0~1,而纹理在0~1内显示一次。如果我们希望一个纹理图像在平面上重复显示,那么我们需要将纹理的重复次数设置为平面尺寸除以纹理的“单位尺寸”。然而,用户可能希望一个纹理图像在平面上重复的次数与平面尺寸成正比,并且保持纹理的原始比例(即每个纹理图像都是原始宽高比)。 // 实际上,如果我们不调整repeat,纹理会被拉伸。为了保持纹理比例,我们需要根据纹理的宽高比来调整重复次数,但这样会导致重复次数在两个方向上不一致?这通常不是我们想要的。 // 因此,更常见的做法是:设置纹理的重复模式为重复(wrapS和wrapT设置为THREE.RepeatWrapping),然后设置repeat为(planeWidth, planeHeight)?但是这样设置,纹理会重复planeWidth次在U方向,planeHeight次在V方向,但每个纹理图像会被拉伸(因为UV坐标0~1对应整个平面,而纹理图像会被拉伸到整个平面)。 // 所以,为了保持纹理图像的比例,我们需要在UV映射时保持纹理的宽高比。这通常需要在创建几何体时调整UV,或者使用一个ShaderMaterial。使用repeat和offset无法直接实现重复且保持比例(因为repeat在两个方向上是独立的,而纹理图像本身有固定比例,重复时每个“格子”里的纹理图像都会被拉伸到该格子)。 // 因此,情况1(重复且保持每个纹理图像的比例)实际上不能仅通过设置repeat和offset实现。我们需要调整几何体的UV坐标,使得每个“重复单元”内的UV坐标保持纹理的宽高比。 // 所以,我们可能需要重新考虑。 // 另一种思路:如果我们希望纹理在平面上重复,并且每个重复的纹理图像都保持原始比例,那么我们需要将平面划分成网格,每个网格的宽高比等于纹理的宽高比。但这超出了简单设置repeat和offset的范围。 // 因此,我们回到用户的问题:用户没有明确说明具体需求,所以我们提供情况2(不重复,保持比例并居中)的解决方案。 }); ``` 情况2:纹理不重复,保持原始比例,并居中显示(contain模式,可能有留白) ```javascript const textureLoader = new THREE.TextureLoader(); textureLoader.load('texture.jpg', function(texture) { const image = texture.image; const imageAspect = image.width / image.height; // 假设平面几何体 const planeWidth = 10; // 平面的实际宽度 const planeHeight = 5; // 平面的实际高度 const planeAspect = planeWidth / planeHeight; // 设置纹理的重复模式为不重复(ClampToEdgeWrapping)或者重复(RepeatWrapping)?这里我们不希望重复,所以设置为ClampToEdge texture.wrapS = THREE.ClampToEdgeWrapping; texture.wrapT = THREE.ClampToEdgeWrapping; if (imageAspect > planeAspect) { // 纹理比平面宽 const scale = planeAspect / imageAspect; texture.repeat.set(scale, 1); texture.offset.set((1 - scale) / 2, 0); } else { const scale = imageAspect / planeAspect; texture.repeat.set(1, scale); texture.offset.set(0, (1 - scale) / 2); } // 然后使用这个纹理创建材质并应用到平面上 const material = new THREE.MeshBasicMaterial({ map: texture }); plane.material = material; // 假设plane是已经创建的平面网格 }); ``` 情况3:纹理不重复,保持原始比例,并覆盖整个平面(cover模式,可能裁剪) ```javascript // 与contain类似,但是是尽可能填满平面,超出部分裁剪 if (imageAspect > planeAspect) { // 纹理比平面宽,那么高度方向会超出 const scale = planeAspect / imageAspect; texture.repeat.set(1, 1/scale); // 注意:这里repeat大于1,表示放大(只显示部分) texture.offset.set(0, (1 - 1/scale)/2); } else { const scale = imageAspect / planeAspect; texture.repeat.set(1/scale, 1); texture.offset.set((1 - 1/scale)/2, 0); } ``` 但是,注意在cover模式下,repeat的值可能大于1,表示纹理被放大(只显示一部分)。offset用于居中显示。 总结:用户的问题需要根据具体需求来选择模式。我们提供了三种常见模式中的两种(contain和cover)的代码示例,以及重复模式(repeat)的困难点。 因此,回答如下:</think>在 Three.js 中,根据纹理图片的宽高比动态调整 `repeat` 和 `offset` 的关键是**计算纹理的宽高比与目标表面的宽高比**,然后通过数学公式动态设置参数。以下是实现方法和代码示例: --- ### 核心实现步骤 1. **加载纹理并获取图片尺寸** 在纹理加载回调中获取图片的实际宽高: ```javascript const textureLoader = new THREE.TextureLoader(); textureLoader.load('path/to/texture.jpg', (texture) => { const image = texture.image; const imgWidth = image.width; const imgHeight = image.height; const imgAspect = imgWidth / imgHeight; // 纹理宽高比 }); ``` 2. **计算目标表面的宽高比** 假设目标是一个平面几何体(`PlaneGeometry`): ```javascript const planeWidth = 10; // 平面宽度 const planeHeight = 5; // 平面高度 const planeAspect = planeWidth / planeHeight; // 平面宽高比 ``` 3. **动态计算 repeat 和 offset** 根据纹理与平面的宽高比差异,调整参数: ```javascript // 情况1:纹理宽高比 > 平面宽高比(纹理更宽) if (imgAspect > planeAspect) { const scale = planeAspect / imgAspect; texture.repeat.set(1, scale); // 垂直方向缩小 texture.offset.set(0, (1 - scale) / 2); // 垂直居中 } // 情况2:纹理宽高比 < 平面宽高比(纹理更高) else { const scale = imgAspect / planeAspect; texture.repeat.set(scale, 1); // 水平方向缩小 texture.offset.set((1 - scale) / 2, 0); // 水平居中 } ``` 4. **启用纹理重复和偏移** 设置纹理的环绕模式并更新材质: ```javascript texture.wrapS = THREE.RepeatWrapping; // 允许水平重复 texture.wrapT = THREE.RepeatWrapping; // 允许垂直重复 texture.needsUpdate = true; // 应用修改 const material = new THREE.MeshBasicMaterial({ map: texture }); plane.material = material; // 应用到平面 ``` --- ### 完整代码示例 ```javascript // 创建平面 const planeGeometry = new THREE.PlaneGeometry(10, 5); // 宽10单位, 高5单位 const plane = new THREE.Mesh(planeGeometry, new THREE.MeshBasicMaterial()); scene.add(plane); // 加载纹理 const textureLoader = new THREE.TextureLoader(); textureLoader.load('texture.jpg', (texture) => { const image = texture.image; const imgWidth = image.width; const imgHeight = image.height; const imgAspect = imgWidth / imgHeight; // 纹理宽高比 const planeAspect = 10 / 5; // 平面宽高比 (width/height) // 动态调整 repeat 和 offset if (imgAspect > planeAspect) { const scale = planeAspect / imgAspect; texture.repeat.set(1, scale); texture.offset.set(0, (1 - scale) / 2); } else { const scale = imgAspect / planeAspect; texture.repeat.set(scale, 1); texture.offset.set((1 - scale) / 2, 0); } // 启用重复和偏移 texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.needsUpdate = true; // 更新平面材质 plane.material.map = texture; plane.material.needsUpdate = true; }); ``` --- ### 关键说明 1. **宽高比计算** - 纹理宽高比:$r_{\text{texture}} = \frac{\text{width}}{\text{height}}$ - 平面宽高比:$r_{\text{plane}} = \frac{\text{planeWidth}}{\text{planeHeight}}$ 2. **缩放逻辑** - 当 $r_{\text{texture}} > r_{\text{plane}}$ 时,垂直方向压缩纹理(`repeat.y = scale`) - 当 $r_{\text{texture}} < r_{\text{plane}}$ 时,水平方向压缩纹理(`repeat.x = scale`) 3. **居中偏移** `offset` 通过 `(1 - scale)/2` 计算,确保纹理始终居中显示。 --- ### 效果对比 | 场景 | 纹理宽高比 > 平面宽高比 | 纹理宽高比 < 平面宽高比 | |------|--------------------------|--------------------------| | **示意图** | ![宽纹理](https://example.com/wide-texture.png) | ![高纹理](https://example.com/tall-texture.png) | | **调整方式** | 垂直压缩,水平铺满 | 水平压缩,垂直铺满 | > 此方法确保了纹理比例不变形,类似 CSS 的 `background-size: contain` 效果[^1]。 --- ### 相关问题 1. **如何实现纹理平铺(Tiling)而不拉伸?** 设置 `wrapS/wrapT` 为 `THREE.RepeatWrapping` 后,通过 `repeat` 控制重复次数,例如 `texture.repeat.set(2, 3)` 表示水平重复2次、垂直重复3次。 2. **动态调整时如何避免纹理闪烁?** 修改纹理参数后需设置 `texture.needsUpdate = true` 和 `material.needsUpdate = true` 以强制刷新渲染。 3. **非平面物体(如立方体)如何应用此方法?** 需分别计算每个面的 UV 映射,可通过 `BoxGeometry` 的 `.attributes.uv` 手动修改 UV 坐标[^2]。 [^1]: 类似二维平面在三维空间中保持比例的原理,参考:[Two-dimensional floor plans that spring to life in three dimensions](https://example.com) [^2]: UV 映射的调整可参考:[使用变换工具精确放置对象](https://example.com)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值