three.js学习之纹理贴图Texture(二)纹理背景的位置移动旋转和重复

three.js学习之纹理贴图Texture中,我们初步了解了three.js中纹理基础知识,包括怎么使用TextureLoader加载一张或者多张纹理图片,如何使用回调函数或者loadimgManager管理纹理图片的加载进度,如何计算纹理图片占用的内存大小。最后,我们提到了使用wrapSwrapT配合rotationrepeatoffset来管理贴图的位置,本篇博文中,我们进一步去探索这些内容及其它一些关于纹理的知识点。

在正式开始之前,还是和以前一样,复制three.js学习之纹理贴图Texture中我们做出来的demo代码,删除除了使用多张纹理贴图的代码,只保留使用我家猫咪代码的那段代码即可。

在保留下来的代码中,先删除掉three.js学习之纹理贴图Texture中所有新添加的代码,只保留基础代码即可。

开始之前,我们先使用PlaneGeometry做一个平面。

1
2
3
4
5
6
7
8
9
10
11
const width = 8;
const height = 8;
cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
const materail = new THREE.MeshBasicMaterial({
map: texture
})

const plane = new THREE.Mesh(planeGeometry, materail)

然后我们停止公用代码中动画的部分,让平面静下来,方便我们后续观察。

停止动画代码如下

1
2
// requestAnimationFrame(render) 注释掉这行代码
render()

注释掉render函数中的这些行代码

1
2
3
4
5
6
7
// time *= 0.001
// group['children'].forEach((each, index) => {
// each.rotation.x = -time
// each.rotation.y = -time
// })

// requestAnimationFrame(render)

重新认识Texture中的wrapSwrapT

在默认情况下,新添加的纹理图片会铺满整个平面,这个时候wrapSwrapT实际上并没有什么作用。它两的用处是当新添加的纹理图片由于某些原因无法填满平面而流出的空白区域应该怎么处理。

wrapSwrapT 有三个可选值来决定这些留出的空白区域的处理方式。

  • THREE.ClampToEdgeWrapping 会将空白区域接触到的第一个像素,也是非空白区域的最后一个像素平铺到空白区域。这个值是默认值,所以当我们不对wrapSwrapT设置的时候,他们的值就是它。
  • THREE.RepeatWrapping 会正常复制重复原有的内容,比如我们原先放的是我家猫的照片,那么空白区域会继续将猫的照片放上去。
  • THREE.MirroredRepeatWrappingTHREE.RepeatWrapping 一样会复制原有内容进行重复,不过会做一个镜像处理。

wrapS被用来决定水平方向(x)怎么处理空白区域
wrapT被用来决定垂直方向(y)怎么处理空白区域

wrapSwrapT制定了规则,当规则被触发时,也会执行对应的动作,这些规则,还是需要有人来触发,接下来我们一起了解一下触发这些行为的规则。

重新认识three.js中Texture纹理贴图下的repeat属性

毫无疑问,repeat就是用来设置纹理贴图重复用的。同wrapSwrapT一样,repeat通过x和y决定水平方向和垂直方向纹理贴图的重复方式。

有两种方式去设置x和y的值,第一种是直接设置,像这样texture.repeat.x = 2texture.repeat.y = 2。第二种是使用set函数来设置,像这样texture.repeat.set(x,y),或者这样texture.repeat.setX(2),texture.repeat.setY(2)

repeatwrapSwrapT配合使用时,可以这样理解,当我们在水平或者垂直方向上设置重复n次时,就相当于将平面在对应方向切割成了n块,除了第一块是原来的纹理图片外,其余的都是空白区域,这些空白区域,将会接受wrapSwrapT设置的规则进行改变。

下面进入实战环节

  • three.js 中texture的参数wrapSwrapT的值是THREE.ClampToEdgeWrappingrepeat的不同表现

    • 设置repeat的x参数

      修改代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const width = 8;
      const height = 8;
      cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

      const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
      texture.repeat.x = 2 // 默认情况下,添加这一行即可,也可以用texture.repeat.setX(2)进行设置
      const materail = new THREE.MeshBasicMaterial({
      map: texture
      })
      const plane = new THREE.Mesh(planeGeometry, materail)

      结果如下

      可以看到,小猫头部好像有一簇毛长了起来。这是因为最后一个像素被无限复制了,因为其它地方颜色都一样,所以看不出明显的异常来。

    • 设置repeat的y参数

      接下来我们同将垂直方向(y)设置重复看看情况

      1
      2
      // texture.repeat.x = 2 注释掉这行
      texture.repeat.setY(2) // 等同于texture.repeat.y = 2。这么写只是为了正面两个方法都可以用。

      这个时候,我们发现小猫的背部也被拉长了。

      wrapSwrapT的参数是ClampToEdgeWrapping时,正如我们开头描述的一样,空白区域被最后一个像素填充来,前面的例子都是只单一的改变x或者y的情况,接下来我们再看看二者同时改变的情况。

    • 同时设置repeat的x和y参数

      将前面注释掉的代码释放出来,如下

      1
      2
      texture.repeat.x = 4 
      texture.repeat.setY(4) // 等同于texture.repeat.y = 2。这么写只是为了正面两个方法都可以用。

      在当前的示例中,我们将repeat的参数x和y都设置成了4,也就是横向和纵向上平面被分成了4份,猫的身体也缩小到了原来的四分之一,其余四分之三则被最后一个像素所占领。

      接下来我们看看相同状况下改变wrapSwrapT会有什么不一样的情况发生

  • wrapSwrapT的参数是THREE.RepeatWrappingrepeat的不同表现

    • 设置repeat的x参数

      修改代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      const width = 8;
      const height = 8;
      cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

      const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
      texture.wrapS = THREE.RepeatWrapping
      texture.wrapT = THREE.RepeatWrapping
      texture.repeat.x = 2 // 默认情况下,添加这一行即可,也可以用texture.repeat.setX(2)进行设置
      const materail = new THREE.MeshBasicMaterial({
      map: texture
      })
      const plane = new THREE.Mesh(planeGeometry, materail)

      可以看到,纹理背景在水平(x)方向上分成了左右两个。

      • 设置repeat的y参数

        修改代码
        1
        2
        // texture.repeat.x = 2 注释掉这行
        texture.repeat.setY(2) // 等同于texture.repeat.y = 2。这么写只是为了正面两个方法都可以用。

      可以看到,背景分成了上下(y方向)两个

      • 同时设置repeat的x和y参数

        1
        2
        texture.repeat.x = 4 
        texture.repeat.y = 4

      怎么回事,怎么出现了这么多?其实很简单,我们在横向纵向上都重复了4次,4*4=16。所以一下子在背景上出现了14个猫。之所以在wrapSwrapT等于ClampToEdgeWrapping时我们只是复制最后一个像素哦,所以感觉没有那么强烈。

  • wrapSwrapT的参数是THREE.MirroredRepeatWrappingrepeat的不同表现

    • 设置repeat的x参数

      修改代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      const width = 8;
      const height = 8;
      cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

      const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
      texture.wrapS = THREE.MirroredRepeatWrapping
      texture.wrapT = THREE.MirroredRepeatWrapping
      texture.repeat.x = 2 // 默认情况下,添加这一行即可,也可以用texture.repeat.setX(2)进行设置
      const materail = new THREE.MeshBasicMaterial({
      map: texture
      })
      const plane = new THREE.Mesh(planeGeometry, materail)

      可以看到,纹理背景在垂直(x)方向上分成了左右两个,而且猫是头对着头,这意味着什么呢?镜像了呀。

    • 设置repeat的y参数

      修改代码

      1
      2
      // texture.repeat.x = 2 注释掉这行
      texture.repeat.setY(2) // 等同于texture.repeat.y = 2。这么写只是为了正面两个方法都可以用。

      可以看到,背景分成了上下(y方向)两个,背对背了。

    • 同时设置repeat的x和y参数

      1
      2
      texture.repeat.x = 4 
      texture.repeat.y = 4

      14只猫,两两背对背,脚对脚了。

到这里,three.js中texture的参数中wrapSwrapTrepeat互相配合使用的效果我们就探索完了。接下来我们去探索three.js中texture的参数wrapSwrapTrotation配合使用的情况。

重新认识three.js中Texture纹理贴图的属性rotation属性

rotation用来控制纹理背景绕着原点旋转多少弧度。1弧度等于180度,因此1 * Math.PI 就是一弧度。

three.js texture rotation 旋转

three.js中texture的纹理贴图旋转默认是以面的左下角为圆心进行旋转,非矩形类的面,也遵循这个规则,只是这个左下角稍微不好找一点,实战时随时观察即可。如上图,图中红色部分是旋转后剩余的空白区域,绿色部分则是已经被旋转的区域,蓝色线框框起来的部分,是实际显示的区域。在旋转过程中,空白区域将会遵循wrapSwrapT设定的值所规定的规则进行填充。接下来咱们一起去验证一下。

对于rotation的赋值,只能使用teture.rotation= 0.1 * Math.PI这样的方式进行。

  • three.js 中texture的参数wrapSwrapT的值是THREE.ClampToEdgeWrappingrotation的不同表现

修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
const width = 8;
const height = 8;
cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

const texture = await load('https://picture.wuhoushu.com/blog/mao.png')
texture.wrapS = THREE.ClampToEdgeWrapping
texture.wrapT = THREE.ClampToEdgeWrapping
texture.rotation = 0.1 * Math.PI
const materail = new THREE.MeshBasicMaterial({
map: texture
})
const plane = new THREE.Mesh(planeGeometry, materail)

可以看到,小猫不再是横向趴着,从头到嘴这一范围(空白区域)内有明显的单元素延伸迹象。

  • three.js 中texture的参数wrapSwrapT的值是THREE.RepeatWrappingrotation的不同表现

    修改代码:

    1
    2
    texture.wrapS = THREE.ClampToEdgeWrapping
    texture.wrapT = THREE.ClampToEdgeWrapping

    改为

    1
    2
    texture.wrapS = THREE.RepeatWrapping
    texture.wrapT = THREE.RepeatWrapping

    可以看到,空白区域出现了猫身体的一部分内容,这是因为旋转后空白区域被分割成不同的区域,纹理背景重复出现,符合预期。

  • three.js 中texture的参数wrapSwrapT的值是THREE.MirroredRepeatWrappingrotation的不同表现

修改代码:

1
2
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping

改为
1
2
texture.wrapS = THREE.MirroredRepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping

小猫背对背,嘴对嘴的再次出现,完美符合预期。

最后,我们再看一下 wrapSwrapToffset的配合

重新认识three.js中Texture纹理贴图的属性offset

offset用来决定纹理贴图朝着某个方向偏移多少距离,它有两个值,分别是x和y。同repeat一样,offset也可以通过两种方法设置,第一种就是直接赋值,第二种是通过set进行设置,取值范围在0到1之间,0是不移动,1是整体完全移动出去。

  • three.js 中texture的参数wrapSwrapT的值是THREE.ClampToEdgeWrappingoffset的不同表现

    • 设置offset的x参数

      修改代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const width = 8;
      const height = 8;
      cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

      const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
      texture.offset.x = 0.3 // 默认情况下,添加这一行即可,也可以用texture.offset.setX(0.3)进行设置
      const materail = new THREE.MeshBasicMaterial({
      map: texture
      })
      const plane = new THREE.Mesh(planeGeometry, materail)

      结果如下

      可以看到,小猫头部好像有一簇毛长了起来。这是因为最后一个像素被无限复制了,因为其它地方颜色都一样,所以看不出明显的异常来。感觉和repeat非常类似

    • 设置offset的y参数

      接下来我们同将垂直方向(y)设置重复看看情况

      1
      2
      // texture.offset.x = 0.3 注释掉这行
      texture.offset.setY(0.3) // 等同于texture.offset.y = 2。这么写只是为了正面两个方法都可以用。

      这个时候,我们发现小猫的背部也被拉长了。是不是和repeat的参数等于2时非常像。

      wrapSwrapT的参数是ClampToEdgeWrapping时,正如我们开头描述的一样,空白区域被最后一个像素填充来,前面的例子都是只单一的改变x或者y的情况,接下来我们再看看二者同时改变的情况。

    • 同时设置offset的x和y参数

      将前面注释掉的代码释放出来,如下

      1
      2
      texture.offset.x = 0.3
      texture.offset.setY(0.3)

      当前的示例中,小猫的北部和头部都被无限拉长了。

      接下来我们看看相同状况下改变wrapSwrapT会有什么不一样的情况发生

  • wrapSwrapT的参数是THREE.RepeatWrappingoffset的不同表现

    • 设置offset的x参数

      修改代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      const width = 8;
      const height = 8;
      cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

      const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
      texture.wrapS = THREE.RepeatWrapping
      texture.wrapT = THREE.RepeatWrapping
      texture.offset.x = 0.3 // 默认情况下,添加这一行即可,也可以用texture.offset.setX(0.3)进行设置
      const materail = new THREE.MeshBasicMaterial({
      map: texture
      })
      const plane = new THREE.Mesh(planeGeometry, materail)

      可以看到,纹理背景在水平(x)方向上分成了左右两个。只不过右边稍微小点,因为咱们移动的距离时0.3嘛

      • 设置repeat的y参数

        修改代码
        1
        2
        texture.offset.x = 0.3
        texture.offset.setY(0.3)

      可以看到,背景分成了上下(y方向)两个

      • 同时设置offset的x和y参数

        1
        2
        texture.offset.x = 0.5
        texture.offset.y = 0.5

      小猫出现在了四个不同的地方,如果这些地方足够大,将是四只完整的猫。

  • wrapSwrapT的参数是THREE.MirroredRepeatWrappingoffset的不同表现

    • 设置offset的x参数

      修改代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      const width = 8;
      const height = 8;
      cosnt planeGeometry = new THREE.PlaneGeometry(width, height)

      const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
      texture.wrapS = THREE.MirroredRepeatWrapping
      texture.wrapT = THREE.MirroredRepeatWrapping
      texture.offset.x = 0.3
      const materail = new THREE.MeshBasicMaterial({
      map: texture
      })
      const plane = new THREE.Mesh(planeGeometry, materail)

      可以看到,纹理背景在垂直(x)方向上分成了左右两个,而且猫是头对着头,这意味着什么呢?镜像了呀。

    • 设置offset的y参数

      修改代码

      1
      2
      // texture.offset.x = 0.3 注释掉这行
      texture.repeat.setY(0.3) 。

      可以看到,背景分成了上下(y方向)两个,背对背了。

    • 同时设置offset的x和y参数

      1
      2
      texture.offset.x = 0.3
      texture.offset.y = 0.3

      4只猫,两两背对背,脚对脚了。

到这里,three.js中texture的参数中wrapSwrapToffset互相配合使用的效果我们就探索完了。但是咱们只是做了简单的探索,将纹理图片放到不同的立体图形上时,虽然基本上和现在一致,但因为立体图形的多样性,还是会有很多的差别。这些我们后面都会找时间进行进一步的探索,如果你有时间,也可以在本文的基础上自己去探索一下。

three.js中关于纹理背景移动,旋转,重复布局的问题,我们在这里也探索完了,下面还是一如即往的奉上完整源码,供诸君参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<script>
function getSearchObj (key) {
const search = window.location.search.replace('?', '')
const list = search.split('&')
const obj = {}
for (let i = 0; i < list.length; i++) {
const eachList = list[i].split('=')
obj[eachList[0]] = eachList[1]
}
return obj[key]
}
async function main () {
const search = location.search
const canvas = document.querySelector('#three')
const renderer = new THREE.WebGLRenderer({canvas})
const fov = 75;
const aspect = 2;
const near = 0.1;
const far = 200;

const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)

const group = new THREE.Group()
group.position.z = -10
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, linecap:'round' })
const loader = new THREE.TextureLoader()
const load = (url) => {
return new Promise((resolve, reject) => {
loader.load(url, (texture) => {
resolve(texture)
})
})
}

{
const width = 16;
const height = 8;
const planeGeometry = new THREE.PlaneGeometry(width, height);
const texture = await load('https://picture.wuhoushu.com/blog/mao.png');
const wraps = getSearchObj('wrapS') ? getSearchObj('wrapS') : 'ClampToEdgeWrapping'
const wrapt = getSearchObj('wrapT') ? getSearchObj('wrapT') : 'ClampToEdgeWrapping'
texture.wrapS = THREE[wraps]
texture.wrapT = THREE[wrapt]

const repeatY = getSearchObj('repeatY') ? getSearchObj('repeatY') : 1
texture.repeat.y = repeatY

const repeatX = getSearchObj('repeatX') ? getSearchObj('repeatX') : 1
texture.repeat.x = repeatX

const rotation = getSearchObj('rotation') ? getSearchObj('rotation') : 0

texture.rotation = rotation * Math.PI

const offsetX = getSearchObj('offsetX') ? getSearchObj('offsetX') : 0
const offsetY = getSearchObj('offsetY') ? getSearchObj('offsetY') : 0

texture.offset.x = offsetX
texture.offset.y = offsetY

const material = new THREE.MeshBasicMaterial({
map: texture
})

const plane = new THREE.Mesh(planeGeometry, material)
group.add(plane)
}

const scene = new THREE.Scene()

{
const lightColor = 0xffffff; // 灯光颜色
const intensity = 1; // 灯光强度
const light = new THREE.DirectionalLight(lightColor, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
scene.add(group)

function resizeRenderSizeToDisplaySize () {
const pixelRatio = window.devicePixelRatio;
const displayWidth = canvas.clientWidth * pixelRatio
const displayHeight = canvas.clientHeight * pixelRatio
const renderWidth = canvas.width;
const renderHeight = canvas.height;
const needResize = displayWidth !== renderWidth || displayHeight !== renderHeight
return needResize
}
function render (time) {
if (resizeRenderSizeToDisplaySize()) {
const pixelRatio = window.devicePixelRatio;
const width = canvas.clientWidth * pixelRatio
const height = canvas.clientHeight * pixelRatio
renderer.setSize(width, height, false)
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}

// time *= 0.001
// group['children'].forEach((each, index) => {
// each.rotation.x = -time
// each.rotation.y = -time
// })

// group.position.y = time
// group.position.z = time
renderer.render(scene, camera)
// requestAnimationFrame(render)
}
// requestAnimationFrame(render)
render()
}
main()
</script>