10.Three.js 基础之纹理
10 Three.js 基础之纹理
纹理(Texture) 概要
前面学习的 材质
而 纹理
- 使用纹理加载器
TextureLoader 加载外部图片(.jpg 或.png) - 通过设置 物体的
.map 属性,将加载得到的外部图片贴合在物体表面
有些
3D 软件教程会直接将 纹理 称呼为 皮肤、贴图
纹理加载器的简单示例代码:
const loader = new Three.TextureLoader()
const material = new Three.MeshBasicMaterial({
map:loader.load('xxx/xx.jpg')
})
补充一点:
- 在汉语词语中,纹最初指 乌龟壳上的纹路、理最初指 石头上的纹路和细腻程度
- 在物理中,纹理指物体表面凸凹不平的沟纹
- 在
Three.sj 中,纹理指 物体光滑表面上的彩色图案
由于纹理牵扯到 图片资源加载,所以我们先补充一下
在React+TypeScript 中引入图片资源
在
第一件要做的事情:src/global.d.ts
declare module '*.png';
declare module '*.gif';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.svg';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.styl';
declare module 'react-app-rewire-alias';
我们先要确保项目源码中
例如:declare module '*.jpg';
是告诉
补充说明:
declare module '*.jpg';
上面那行声明代码是一种简写,实际他表示的代码为:
declare module '*.jpg' {
const content: string;
export default content;
}
第1 种:使用import 引入和使用图片
假设图片文件位置为
import imgSrc from '../assets/imgs/mapping.jpg'
使用该图片,代码为:
<img src={imgSrc}>
请注意
imgSrc 的实际类型为string ,imgSrc 是react 编译后该图片资源对应的位置
经过
React 编译之后,图片位置最终变为:static/media/mapping.xxxxxxxx.jpg
第2 种:使用require 引入和使用图片
不需要在顶部
<img src={require('../assets/imgs/mapping.jpg').default}>
请注意,一定要 要有
.default 才可以
当需要批量引入很多张图片时,或者动态获得 引入的图片地址时,第
2 种写法就非常方便
问题延展:
你是否还记得在讲解 图元 中
const url = 'https://threejsfundamentals.org/threejs/resources/threejs/fonts/helvetiker_regular.typeface.json'
那如果 字体数据文件
-
首先可以肯定的是,引入图片资源的方式并不适用于
.json 文件资源.json 文件 对应的webpack 加载器为json-loader ,他是可以直接将json 中数据内容解析出来直接给我们使用的.jpg 文件对应的是webpack 加载器为file-loader ,他仅仅是将资源更换目录和重命名,并不会对图片( 图片也不需要) 内容解析的 -
那该怎么引入?
这个问题我们知识先思考一下,暂时先不去讲如何解决。
让我们回到
示例1 :加载一张图,实现一个有贴图的立方体
示例目标
- 创建一个正方体
- 通过
TextureLoader 加载一张外部图片 - 将图片贴在立方体的
6 个面上
具体示例编写
第
网上随便找了一张风景图,存放在项目
第
修改项目中
"@/assets/*": ["./src/assets/*"]
备注:以后的章节中,我们还会陆续向
assets 目录中添加其他类型的文件,例如模型文件、asc 数据文件等等。
第
在项目
import { useEffect, useRef } from 'react'
import * as Three from 'three'
import './index.scss'
import imgSrc from '@/assets/imgs/mapping.jpg' //引入图片资源
const HelloTexture = () => {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (canvasRef.current === null) {
return
}
const renderer = new Three.WebGLRenderer({ canvas: canvasRef.current as HTMLCanvasElement })
const camera = new Three.PerspectiveCamera(40, 2, 0.1, 1000)
camera.position.set(0, 0, 40)
const scene = new Three.Scene()
scene.background = new Three.Color(0xcccccc)
//创建一个 纹理加载器
const loader = new Three.TextureLoader()
//创建一个材质,材质的 map 属性值为 纹理加载器加载的图片资源
const material = new Three.MeshBasicMaterial({
map: loader.load(imgSrc) //loader.load('xxx.jpg')返回值为Three.Text类型实例
})
const box = new Three.BoxBufferGeometry(8, 8, 8)
const mesh = new Three.Mesh(box, material)
scene.add(mesh)
const render = (time: number) => {
time = time * 0.001
mesh.rotation.x = time
mesh.rotation.y = time
renderer.render(scene, camera)
window.requestAnimationFrame(render)
}
window.requestAnimationFrame(render)
const resizeHandle = () => {
const canvas = renderer.domElement
camera.aspect = (canvas.clientWidth / canvas.clientHeight)
camera.updateProjectionMatrix()
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false)
}
resizeHandle()
window.addEventListener('resize', resizeHandle)
return () => {
window.removeEventListener('resize', resizeHandle)
}
}, [canvasRef])
return (
<canvas ref={canvasRef} className='full-screen' />
)
}
export default HelloTexture
查看最终效果,终端中执行:yarn start
若一切正常,就会看到一个包含风景贴图的立方体。
示例2 :加载多张图片,实现一个骰子
在上面的示例中,我们是加载了一张图片资源,然后立方体默认
那么假设我们希望立方体每个面都使用不同的图片来进行贴图,还如何实现呢?
骰子的特征就是:立方体6 个面贴图均不相同
骰
(tóu) 子,也就是 色(sh ǎi) 子。
实现方式
在之前的代码中,我们创建物体,也就是网格的代码是:
const mesh = new Three.Mesh(box, material)
请注意,上述代码中
第1 步:添加 骰子 的6 个面图片资源
假设我们已经有 骰子
- src/assets/imgs/dice0.jpg
- ….
- src/assets/imgs/dice5.jpg
其中
第2 步:引入6 个图片资源,并创建材质数组
第1 种方式:使用import 引入图片:
依次引入
import imgSrc0 from '@/assets/imgs/dice0.jpg'
import imgSrc1 from '@/assets/imgs/dice1.jpg'
import imgSrc2 from '@/assets/imgs/dice2.jpg'
import imgSrc3 from '@/assets/imgs/dice3.jpg'
import imgSrc4 from '@/assets/imgs/dice4.jpg'
import imgSrc5 from '@/assets/imgs/dice5.jpg'
创建材质数组,元素为骰子的
//创建一个纹理加载器
const loader = new Three.TextureLoader()
const imgSraArr = [imgSrc0, imgSrc1, imgSrc2, imgSrc3, imgSrc4, imgSrc5]
//创建一组材质,每个材质对应立方体每个面所用到的材质
const materialArr: Three.MeshBasicMaterial[] = []
imgSraArr.forEach((src) => {
materialArr.push(new Three.MeshBasicMaterial({
map: loader.load(src)
}))
})
第2 种方式:使用require 引入图片:
实际上使用的是
//不需要在顶部 import xxx from 'xxx.jpg'
//创建一个 纹理加载器
const loader = new Three.TextureLoader()
//创建 6 个面对应的材质
const materialArr: Three.MeshBasicMaterial[] = []
for (let i = 0; i < 6; i++) {
materialArr.push(new Three.MeshBasicMaterial({
map: loader.load(require(`@/assets/imgs/dice${i}.jpg`).default)
}))
}
第3 步:创建网格时,使用 材质数组materialArr
const box = new Three.BoxBufferGeometry(8, 8, 8)
const mesh = new Three.Mesh(box, materialArr) //注意,此处使用的不再是单个材质,而是一个材质数组
scene.add(mesh)
最终实际运行后,立方体
你以为完事了?
冷静观察并思考:骰子数字的分布
经过观察,你会发现,骰子数字目前的分布为:
- 数字
1 对面是 数字2 - 数字
3 对面是 数字4 - 数字
5 对面是 数字6
现实中的骰子数字分布规则:
- 数字
1 对面是 数字6 - 数字
2 对面是 数字5 - 数字
3 对面是 数字4
简而言之就是 对立面
如果想模拟出真实的骰子数字分布,那么就要按照这个规则去修改图片编号对应的图片数字。
加载多张图作为材质的补充说明:
上面示例
问题
答:缺少
创建网格时:
- 若
Three.Mesh(xx,xx) 构造函数中第2 个参数值是 单个 材质,则图元实例上所有的面均采用该材质( 纹理) - 若第
2 个参数值是 材质组成的数组,则该数组中材质数量不能少于图元实例各个可渲染面的数量。
问题
答:支持多个材质纹理的图元,可需要渲染的面数量也不同,例如圆锥体只需要
不是所有材质都支持多个材质纹理的,例如平面圆
问题
精灵图也被称为雪碧图,比如在网页
CSS 中,可以将多个小图标图片合并放在一张图片上,当不同地方需要使用不同的图标图片时,通过设置图片中的图标对应的位置来只显示该图标。这样做的好处是可以让
N 个小图片资源请求 合并为1 个图片资源请求,降低服务器请求压力。
答:
实现方式就是将多张图片合并成一张图片,然后创建成一个 纹理图集
示例3 :纹理加载器的不同事件回调函数
让我们在回到 示例
//创建一个 纹理加载器
const loader = new Three.TextureLoader()
/创建一个材质,材质的 map 属性值为 纹理加载器加载的图片资源
const material = new Three.MeshBasicMaterial({
map: loader.load(require('@/assets/imgs/mapping.jpg').default)
})
上述代码中,我们只是设置了加载图片资源的文件路径,并没有添加图片加载相关的事件处理回调函数。
关于纹理加载器
TextureLoader.load(
url: string,
onLoad?: ((texture: Three.Texture) => void) | undefined,
onProgress?: ((event: ProgressEvent<EventTarget>) => void) | undefined,
onError?: ((event: ErrorEvent) => void) | undefined
): Three.Texture
为了显示更多纹理图片加载过程中的细节,我们可以将代码修改为:
//创建一个 纹理加载器
const loader = new Three.TextureLoader()
const material = new Three.MeshBasicMaterial({
map: loader.load(require('@/assets/imgs/mapping.jpg').default,
(texture) => {
console.log('纹理图片加载完成')
console.log(texture)
console.log(texture.image.currentSrc) //此处即图片实际加载地址
},
(event) => {
console.log('纹理图片加载中...')
console.log(event)
},
(error) => {
console.log('纹理图片加载失败!')
console.log(error)
}
)
})
请注意:
-
在项目调试过程中,由于图片资源位于本地,所以加载所需时间极其短暂。
-
若项目加载的是网络图片资源,但是由于目前一般网速都比较快,一张
100K 左右的图片下载所需时间非常短暂,所以可能在即运行中,根本触发不了onProgress 处理函数。此处是存疑的,因为按照我的理解,就算是图片资源加载再快,
onProgress 回调函数至少也应该执行一次才对的,可事实是onProgress 回调函数一次也没有被调用。
补充说明:谷歌浏览器网络调试
为了能够模拟出网络加载速度较慢的情况,可以通过设置 谷歌浏览器 调试工具中的 网络面板。
- 勾选上
Disable cache ( 禁用缓存) - 将网络由
Online 修改为自定义网络网速模式,在创建的自定义网速模式中可是设置下载或上传的网速。
示例4 :使用纹理加载管理器监控多个图片资源的加载
在示例
假设我们需要加载多个 纹理图片资源,可以新建一个
具体代码示例:
//创建所有纹理加载的管理器
const loadingManager = new Three.LoadingManager()
//创建一个 纹理加载器
const loader = new Three.TextureLoader(loadingManager)
//创建 6 个面对应的材质
const materialArr: Three.MeshBasicMaterial[] = []
for (let i = 0; i < 6; i++) {
materialArr.push(new Three.MeshBasicMaterial({
map: loader.load(require(`@/assets/imgs/dice${i}.jpg`).default)
}))
}
//添加加载管理器的各种事件处理函数
loadingManager.onLoad = () => {
console.log('纹理图片资源加载完成')
}
loadingManager.onProgress = (url, loaded, total) => {
console.log(`图片加载中, 共 ${total} 张,当前已加载 ${loaded} 张 ${url}`)
}
loadingManager.onError = (url) => {
console.log(`加载失败 ${url}`)
}
请注意,
onProgress 中的loaded 和total 分别指:
- loaded:已加载完成的纹理图片数量
- total:总共需要加载的纹理图片数量
补充说明:
在上面代码中,虽然是先执行的
纹理占用的内存
只与图片尺寸( 宽高) 有关,与图片文件体积无关
原则1 :纹理图片文件的体积只影响加载完成所需时间
加载完成所需时间是由 服务器带宽和你本机下载速度 决定的。
原则2 :纹理图片的尺寸( 宽高) 决定所占内存大小
占用内存计算公式:
举例,假设某个纹理图片的尺寸为 宽
原则3 :纹理图片的清晰度( 图片质量) 并不影响所占内存大小
纹理图片所占内存只与图片宽高有关,至于图片清晰度
原则4 :同样尺寸的JPG 和PNG 图片所占内存大小相同
纹理图片所占内存只与图片宽高有关,至于图片是哪种格式
但是请注意,由于
无论哪种方式,关于纹理图片渲染所占内存和相关计算,都是交由
小总结:
- 纹理图片所占内存大小仅和宽高有关,因此在保证贴图清晰度的前提下应尽量减小图片尺寸。
- 纹理图片的格式、清晰度仅会影响加载资源所需时间。
纹理图片尺寸与物体渲染尺寸的关系处理
纹理图片尺寸:是指图片本身的宽高尺寸
物体渲染面尺寸:是指最终在镜头中物体某一个面所渲染出的尺寸
物体渲染尺寸是由 物体本身大小和物体距离镜头的远近来共同决定的。
纹理图片尺寸和物体渲染面尺寸几乎是不可能刚好完全相同的。
这个几率几乎不存在
那么我们就需要考虑,假设
首先我们先了解一下
什么是mipmap ?
这样便可以通过计算,不断得到 面积为上一级
而
假设 纹理图片尺寸大于渲染面尺寸,此时需要对纹理进行缩小。
此处为我个人的理解:假设纹理图片尺寸小于渲染面尺寸,那么此时使用相反计算过程,最终得到一个比较模糊但尺寸符合的贴图数据。
这样做的好处是?
答:这种策略其实相当于牺牲掉了纹理贴图的精准性,换来了计算所需性能上的提升。
这也解释了为什么有时候即使纹理图片尺寸非常大,但某些时候渲染出的物体实际上还是略有模糊。
纹理的缩放模式有哪几种?
大体上可以分为
-
NearestFilter
-
LinearFilter
-
Mipmap 相关的模式mipmap 与Nearest 、Linear 的各种结合
缩放模式 | 模式说明 |
---|---|
NearestFilter | 最接近模式,选择最接近的像素 |
NearestMipmapNearestFilter | 选择最贴近目标解析度的 |
NearestMipMapNearestFilter | |
NearestMipmapLinearFilter | 选择层次最近的 |
NearestMipMapLinearFilter | |
LinearFilter | 线性模式,选择 |
LinearMipmapNearestFilter | 选择最贴近目标解析度的 |
LinearMipMapNearestFilter | |
LinearMipmapLinearFilter | 选择层次最近的 |
LinearMipMapLinearFilter |
上述表格中
Mipmap 和MipMap 应该是同一个意思的不同书写方式而已。除了
NearestFilter 和LinearFilter 之外,其他模式都属于Mipmap 模式的变种。
当第一次见到
请记住以下原则:
Nearest:最接近算法,精确度高,像素感比较强烈,锐化程度比较强烈,所用计算量大
Linear:线性算法,精确度不高,像素感不强烈,锐化程度不强,相对比较模糊和平滑,所用计算量小
NearestMipmapNearestFilter、NearestMipmapLinearFilter、LinearMipmapNearestFilter、
修改之前示例中的纹理相关代码:
在之前示例代码中,我们给某个材质添加纹理都是通过:
const loader = new Three.TextureLoader()
const material = new Three.MeshBasicMaterial({
map: loader.load( ... )
})
上面代码中的
为了方便我们设置
const loader = new Three.TextureLoader()
const texture: Three.Texture = loader.load( ... )
const material = new Three.MeshBasicMaterial({
map: texture
})
情况1 :纹理图片尺寸 大于 物体渲染面的尺寸
设置纹理缩小模式的属性:magFilter
当纹理图片尺寸 大于 物体渲染面尺寸时,可以通过设置
-
texture.magFilter = Three.NearestFilter:最接近模式
该模式下最终渲染效果更加清晰,但所需内存更多,渲染时间慢
-
texture.magFilter = Three.LinearFilter:线性模式
该模式下最终渲染效果略微模糊,但所需内存更少,渲染时间快
默认,
注意:纹理图片缩小模式,不可以选择
Mipmap 中的任何一种模式,只能从NearestFilter 、LinearFilter 选择其一。
情况2 :纹理图片尺寸 小于 物体渲染面的尺寸
设置纹理放大模式的属性:minFilter
当纹理图片尺寸 小于 物体渲染面尺寸时,可以通过设置
texture.minFilter = Three.NearestFilet ( 最接近模式) texture.minFilter = Three.LinearFilter ( 线性模式) - texture.minFilter = Three.NearestMipmapNearestFileter
- texture.minFilter = Three.NearestMipmapLinearFilter
- texture.minFilter = Three.LinearMipmapNearestFilter
- texture.minFilter = Three.LinearMipmapLinearFilter
默认,
究竟该选择哪种模式?
纹理需要缩小时,如果对渲染清晰度要求比较高,则选择
纹理需要放大时,如无特殊需要,推荐使用默认的
在不考虑性能的前提下,选择Nearest 是否渲染效果最佳?
事实上,除了性能方面的考虑之外,无非特殊必要,并不推荐使用
原因是若使用
而使用
补充说明:
有一种情况除外:采用纹理重复的方式,用极致的小图去渲染比较大的面
满足以下条件:
- 纹理图片极其小,例如
2 像素X 2 像素 - 希望通过 纹理重复 ,来将比较小的 图片 铺满 整个渲染面
这个时候就推荐使用
例如:用一个
提前预告:下一节我们将讲解 灯光,其中就会用到这个知识点。
纹理的重复、偏移、旋转
默认情况下,纹理是不会重复、偏移和旋转的。
默认将纹理通过 伸缩 以适用于渲染面。
接下来讲一下如何设置纹理的重复、偏移、旋转。
设置重复方式
纹理重复分为:水平重复
重复有
- Three.ClampToEdgeWrapping:每个边缘最后一个像素将永远重复
- Three.RepeatWrapping:重复整个纹理
- Three.MirroredRepeatWrapping:纹理被镜像
( 对称反转) 并重复
设置重复代码:
texture.wrapS= Three.RepeatWrapping
texture.wrapT= Three.RepeatWrapping
设置重复次数
设置重复次数代码:
texture.repeat.set(2,3) //设置水平方向重复 2 次、垂直方向重复 3 次
设置偏移
设置偏移的
设置偏移的代码:
texture.offset.set(0.5,0.25) //设置纹理水平方向偏移 0.5 个纹理宽度、垂直方向偏移 0.25 个纹理高度
0.5 个纹理宽度也就相当于 一半的宽度偏移量
0.25 个纹理高度也就相当于1/4 的高度偏移量
设置旋转
通过修改
在
Three.js 中,有内置的可以将角度转变为弧度的方法
通过修改
纹理的中心点坐标体系,相当于传统的四象限坐标。
中心点的
-
默认的旋转中心点为纹理图片的左下角,坐标为
(0,0) -
坐标的单位为
1 单位= 1 个纹理图片对应大小例如
(0.5, 0.5) 坐标对应的是 纹理图片的中心位置
示例代码:
texture.center.set(0.5,0.5) //将旋转中心点改为图片的正中心位置
texture.rotation = Three.MathUtils.degToRad(45) //设置纹理旋转弧度
//MathUtils.degToRad() 方法作用是将度数转化为弧度
关于纹理,目前就先讲到这里。在以后的学习中,还会对纹理坐标以及其他几种类型的纹理进行学习。
纹理是目前学习中遇到的内容量最大的一节。
好好加油,下一节讲解 灯光。