08-three.js Textures
Three.js Journey — Learn WebGL with Three.jsThe ultimate Three.js course whether you are a beginner or a more advanced developerhttps://threejs-journey.com/?c=p3
使用原生 JavaScript
首先是静态页面的放置位置,如果使用Vite模版配置,可以直接放在 /static/ 文件夹中,引用参考:
const imageSource = '/files/image.png'console.log(imageSource)
简单带过一下使用原生JavaScript的步骤:
- 创建 Image 实例
- 监听 image 的 load 事件
- 将texture变量提升为全局变量,并使用 Texture 类创建纹理
- 设置 image.src 属性加载图片,再将texture变量提升为全局变量
- 将texture应用到material中
const image = new Image()
const texture = new THREE.Texture(image)
texture.colorSpace = THREE.SRGBColorSpace // 修正 `sRGB` 颜色空间造成的纹理色值偏差
image.addEventListener('load', () =>
{texture.needsUpdate = true // 更新在函数外部创建的纹理
})
image.src = '/textures/door/color.jpg'// ...const material = new THREE.MeshBasicMaterial({ map: texture })
用于材质的 map 或 matcap 属性的纹理应该以 sRGB 编码,需要将 colorSpace 设置为 THREE.sRGBColorSpace,但仅限于这些纹理。
总结关键点:
1. texture.colorSpace = THREE.SRGBColorSpace
2. texture.needsUpdate = true
【效果图】
使用 TextureLoader
将上面原生JavaScript的引入改为 THREE.TextureLoader() 引入:
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')
texture.colorSpace = THREE.SRGBColorSpace
可以使用一个 TextureLoader 实例加载任意多的纹理(一对多)。如果现在有多个图片需要加载,并且希望共享事件(比如在所有图片加载完成后接收通知),可以使用THREE.LoadingManager() 与THREE.TextureLoader() 配合:
const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(loadingManager)
如果想要显示加载器并在所有资源加载完成后隐藏它,LoadingManager 对这点非常有用,可以通过将以下属性替换为自己的函数来监听各种事件。
// LoadingManager的使用
const loadingManager = new THREE.loadingManager()
// 可以通过将以下属性替换为自己的函数来监听各种事件
loadingManager.onStart = ()=> {console.log('loading started!');
}
loadingManager.onLoad = ()=> {console.log('loading finished!');
}
loadingManager.onProgress = ()=> {console.log('loading progressing!');
}
loadingManager.onError = ()=> {console.log('loading error!');
}
const TextureLoader = new THREE.TextureLoader(loadingManager)
【扩展】ExrLoader
这次下载的素材中有以 .exr 后缀结尾的文件,经查需要通过 ExrLoader 进行导入,直接用
TextureLoader() 会报错,二者可以一起与LoadingManager 使用,如下:
/*** TextureLoader*/
// LoadingManager的使用
const loadingManager = new THREE.LoadingManager()
// 可以通过将以下属性替换为自己的函数来监听各种事件
loadingManager.onStart = () => {console.log('loading started!')
}
loadingManager.onLoad = () => {console.log('loading finished!')
}
loadingManager.onProgress = () => {console.log('loading progressing!')
}
loadingManager.onError = () => {console.log('loading error!')
}
const TextureLoader = new THREE.TextureLoader(loadingManager)/*** 【扩展】EXRLoader*/
// 使用同一个loadingManager创建加载器
const ExrLoader = new EXRLoader(loadingManager);// 加载所有需要的图片
const textures = {diffTexture: TextureLoader.load('/textures/metal_plate_diff_1k.jpg'),dispTexture: TextureLoader.load('/textures/metal_plate_disp_1k.png'),metalTexture: ExrLoader.load('/textures/metal_plate_metal_1k.exr'),norTexture: ExrLoader.load('/textures/metal_plate_nor_gl_1k.exr'),roughTexture: ExrLoader.load('/textures/metal_plate_rough_1k.exr'),
}textures.diffTexture.colorSpace = THREE.SRGBColorSpace
repeat
textures.diffTexture.colorSpace = THREE.SRGBColorSpace
textures.diffTexture.repeat.x = 2
textures.diffTexture.repeat.y = 3
上面的纹理没有重复,反而显得更小,最后一个像素似乎被拉伸了,因为默认情况下纹理没有设置为重复。通过使用 THREE.RepeatWrapping
常量更新 wrapS
和 wrapT
属性,如下:
textures.diffTexture.colorSpace = THREE.SRGBColorSpace
textures.diffTexture.repeat.x = 2
textures.diffTexture.repeat.y = 3
textures.diffTexture.wrapS = THREE.RepeatWrapping // `wrapS` 用于 `x` 轴重复
textures.diffTexture.wrapT = THREE.RepeatWrapping // `wrapT` 用于 `y` 轴重复
可以使用 THREE.MirroredRepeatWrapping 更改方向(镜像):
textures.diffTexture.colorSpace = THREE.SRGBColorSpace
textures.diffTexture.repeat.x = 2
textures.diffTexture.repeat.y = 3
// textures.diffTexture.wrapS = THREE.RepeatWrapping // `wrapS` 用于 `x` 轴重复
// textures.diffTexture.wrapT = THREE.RepeatWrapping // `wrapT` 用于 `y` 轴重复
textures.diffTexture.wrapS = THREE.MirroredRepeatWrapping
textures.diffTexture.wrapT = THREE.MirroredRepeatWrapping
offset
可以使用 offset 属性来偏移纹理,该属性也是一个 Vector2(具有 x 和 y 属性),更改这些值将简单地偏移 UV 坐标:
textures.diffTexture.offset.x = 0.5
textures.diffTexture.offset.y = 0.5
rotation
旋转基准点见下方旋转基准点说明。
textures.diffTexture.rotation = Math.PI * 0.25
UV展开
纹理以不同的方式被拉伸或挤压以覆盖几何形状,叫做 UV 展开,可以想象成像展开折纸或糖果纸一样将其展平。每个顶点将在一个平面(通常是正方形)上有一个 2D 坐标,可以在 geometry.attributes.uv 属性中看到这些 UV 2D 坐标:
console.log(geometry.attributes.uv)
旋转基准点
`0, 0` UV 坐标在立方体面的左下角,如果想改变旋转时的基准点,可以使用 center 属性,将以立方体中心为旋转点:
textures.diffTexture.rotation = Math.PI * 0.25
textures.diffTexture.center.x = 0.5
textures.diffTexture.center.y = 0.5
过滤和 mip 映射
缩小滤波器
当纹理的像素小于渲染像素时会发生压缩滤镜。换句话说,纹理对于它覆盖的表面来说太大了。
可以通过minFilter
属性来更改纹理的压缩滤镜,有6种可能的值:
-
THREE.NearestFilter
-
THREE.LinearFilter
-
THREE.NearestMipmapNearestFilter
-
THREE.NearestMipmapLinearFilter
-
THREE.LinearMipmapNearestFilter
-
THREE.LinearMipmapLinearFilter 【默认值】
放大滤镜
当纹理的像素大于渲染像素时,换句话说,纹理对于它覆盖的表面来说太小了。
可以使用 magFilter 属性来更改纹理的放大滤镜,有2种可能的值:
-
THREE.NearestFilter
-
THREE.LinearFilter【默认值】
【注意】仅在 minFilter 属性中使用 mipmaps。如果使用的是 THREE.NearestFilter,则不需要 mipmaps,并可以通过 colorTexture.generateMipmaps = false 关闭它们,这将稍微减轻 GPU 的负担:
colorTexture.generateMipmaps = false
colorTexture.minFilter = THREE.NearestFilter
工具网站
压缩图片工具网站
https://tinypng.com/
纹理素材网站
poliigon.com
3dtextures.me
arroway-textures.ch
此次使用到的素材
Metal Plate Texture • Poly Haven
练习
练习,通过按钮切换 material 的 texture
练习,通过按钮切换几何体
【完整代码】
// Texture Link: https://polyhaven.com/a/metal_plateimport * as THREE from 'three'
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import gsap from 'gsap'
import GUI from 'lil-gui'
// console.log(GUI);
// console.log(EXRLoader);/*** TextureLoader*/
// 使用 TextureLoader
// const TextureLoader = new THREE.TextureLoader()
// 可以在路径后发送3个函数。这些函数将在以下事件中被调用,依次分别是
// 1.load 当图像加载成功时 2.progress 当加载进行中时 3.error 如果发生错误
// const texture = TextureLoader.load(
// '/textures/metal_plate_diff_1k.jpg',
// () => {
// console.log('loading finished');// },
// () => {
// console.log('loading progressing');// },
// () => {
// console.log('loading error');// },
// )/*** TextureLoader*/
// LoadingManager的使用
const loadingManager = new THREE.LoadingManager()
// 可以通过将以下属性替换为自己的函数来监听各种事件
loadingManager.onStart = () => {console.log('loading started!')
}
loadingManager.onLoad = () => {console.log('loading finished!')
}
loadingManager.onProgress = () => {console.log('loading progressing!')
}
loadingManager.onError = () => {console.log('loading error!')
}
const TextureLoader = new THREE.TextureLoader(loadingManager)/*** 【扩展】EXRLoader*/
// 使用同一个loadingManager创建加载器
const ExrLoader = new EXRLoader(loadingManager);// 加载所有需要的图片
const textures = {diffTexture: TextureLoader.load('/textures/metal_plate_diff_1k.jpg'),dispTexture: TextureLoader.load('/textures/metal_plate_disp_1k.png'),metalTexture: ExrLoader.load('/textures/metal_plate_metal_1k.exr'),norTexture: ExrLoader.load('/textures/metal_plate_nor_gl_1k.exr'),roughTexture: ExrLoader.load('/textures/metal_plate_rough_1k.exr'),
}textures.diffTexture.colorSpace = THREE.SRGBColorSpace
// textures.diffTexture.repeat.x = 2
// textures.diffTexture.repeat.y = 3
textures.diffTexture.wrapS = THREE.RepeatWrapping // `wrapS` 用于 `x` 轴重复
textures.diffTexture.wrapT = THREE.RepeatWrapping // `wrapT` 用于 `y` 轴重复
// textures.diffTexture.wrapS = THREE.MirroredRepeatWrapping
// textures.diffTexture.wrapT = THREE.MirroredRepeatWrapping// const diffTexture = TextureLoader.load('/textures/metal_plate_diff_1k.jpg')
// const dispTexture = TextureLoader.load('/textures/metal_plate_disp_1k.png')
// const metalTexture = ExrLoader.load('/textures/metal_plate_metal_1k.exr')
// const norTexture = ExrLoader.load('/textures/metal_plate_nor_gl_1k.exr')
// const roughTexture = ExrLoader.load('/textures/metal_plate_rough_1k.exr')// Canvas
const canvas = document.querySelector('canvas.webgl')// Scene
const scene = new THREE.Scene()/*** Object*/
const geometries = {BoxGeometry: new THREE.BoxGeometry(1, 1, 1),SphereGeometry: new THREE.SphereGeometry(1, 32, 32),ConeGeometry: new THREE.ConeGeometry(1, 2, 32),TorusGeometry: new THREE.TorusGeometry(1, 0.4, 16, 100)
};
const geometry = geometries.BoxGeometry
// 使用纹理 texture
const material = new THREE.MeshBasicMaterial({ map: textures.diffTexture })
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)mesh.position.x = 0/*** Sizes*/
const sizes = {width: window.innerWidth,height: window.innerHeight
}/*** Camera*/// 自定义控制
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 1000)
camera.position.z = 3
scene.add(camera)/*** Controls*/
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true/*** Renderer*/
const renderer = new THREE.WebGLRenderer({canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.render(scene, camera)const tick = () => {// Update controlscontrols.update()renderer.render(scene, camera)window.requestAnimationFrame(tick)
}
tick()window.addEventListener('resize', () => {// 1. 更新 sizessizes.width = window.innerWidthsizes.height = window.innerHeight// 2.1 更新 camera aspect 纵横比camera.aspect = sizes.width / sizes.height// 2.2 更新 aspect 时要配合更新投影矩阵 updateProjectionMatrixcamera.updateProjectionMatrix()// 3. 更新 rendererrenderer.setSize(sizes.width, sizes.height)renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))})window.addEventListener('dblclick', () => {// 注释原来的双击全屏if (!document.fullscreenElement) {canvas.requestFullscreen();} else {document.exitFullscreen();}})// 练习// 生成按钮,控制切换
// const buttonLabels = ['diffTexture', 'dispTexture', 'metalTexture', 'norTexture', 'roughTexture']
// const buttonLabels = ['BoxGeometry', 'SphereGeometry', 'ConeGeometry', 'TorusGeometry']
// const buttonLabels = ['偏移', '复原']
const buttonLabels = ['旋转', '复原']
const buttonsHTML = `
<div class="action-buttons" style="position: absolute;">${buttonLabels.map(label =>`<button class="action-btn" style="width: 110px; margin: 16px; cursor: pointer;">${label}</button>`
).join('')}
</div>
`;
// 添加到文档
document.body.insertAdjacentHTML('beforeend', buttonsHTML);// 生成按钮,控制切换 ENDlet currentMesh = mesh; // 保存当前网格对象// 为按钮添加事件
document.querySelectorAll('.action-btn').forEach(btn => {btn.addEventListener('click', function () {// 切换纹理// const textureType = this.textContent// material.map = textures[textureType]// material.needsUpdate = true;// 切换纹理 END// 切换几何体// if (currentMesh && scene.children.includes(currentMesh)) {// scene.remove(currentMesh);// // 可选:释放几何体内存// currentMesh.geometry.dispose();// }// // 创建新的几何体网格// const geometryType = this.textContent;// const geometry = geometries[geometryType];// if (!geometry) {// console.error('未找到几何体:', geometryType);// return;// }// currentMesh = new THREE.Mesh(geometry, material);// scene.add(currentMesh);// // 3. 调试输出// // console.log('当前场景对象:', scene.children);// console.log(geometry.attributes.uv)// 切换几何体 END// 偏移与复原// if (this.textContent == '偏移') {// textures.diffTexture.offset.x = 0.5// textures.diffTexture.offset.y = 0.5// } else {// textures.diffTexture.offset.x = 0// textures.diffTexture.offset.y = 0// }// 旋转与复原if (this.textContent == '旋转') {textures.diffTexture.rotation = Math.PI * 0.25} else {textures.diffTexture.rotation = 0}});
});
项目创建参考
01-three.js vite基础示例_three.js示例-CSDN博客文章浏览阅读392次。three.js 基本示例代码_three.js示例https://blog.csdn.net/gaowxx/article/details/147954918?spm=1001.2014.3001.5501