threejs响应式设计开发

hello three.js中,在默认画布上我们做出了一个小的立方体。默认情况下,three.js中的画布大小是300 * 200,所以我们做出来的东西,在大大的屏幕上也只占小小的一块。在实际开发中,我们可能需要根据实际情况去调整画布的大小,最常见的应该就是让画布满屏了。

这一节当中,我们会做两件事,第一,给应用添加灯光,只需要知道怎么添加就行,不会详细讲;第二,实现three.js的响应式开发。

在正式开始之前,我们首先在learnThree文件夹下新建一个responseThree.html的文件,然后将下面代码复制进去:

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
<!DOCTYPE html>
<html>
<head>
<title>threejs响应式设计开发</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/r99/three.min.js"></script>
</head>
<body>
<canvas id="three"></canvas>
<script>
function main () {
const canvas = document.querySelector('#three')
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2; // 默认值
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

const material = new THREE.MeshBasicMaterial({color: 0x44aa88});

const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);

const cube = new THREE.Mesh(geometry, material);
cube.position.z = -2
const scene = new THREE.Scene();
scene.add(cube)
function render (time) {
time *= 0.001
cube.rotation.x = time
cube.rotation.y = time
renderer.render(scene, camera)
requestAnimationFrame(render)
}
requestAnimationFrame(render)
}
window.onload = main
</script>
</body>
</html>

添加灯光

在浏览器中打开responseThree.html,我们虽然能够是一个立方体在旋转,但还是不够清晰,因此我们先将light灯光给它放进去,然后再继续。

  • 创造灯光
    1
    2
    3
    4
    5
    const lightColor = 0xffffff; // 灯光颜色
    const intensity = 1; // 灯光强度
    const light = new THREE.DirectionalLight(lightColor, intensity);
    light.position.set(-1, 2, 4);
    scene.add(light);
    平行光会将灯光射向原点,但是它自己默认为止也在原点,因此咱们将等的位置移动到立方体左上方一点。然后添加到整个场景中去。
  • 改变材质
    就跟在现实中差不多,有的东西反光,有的则不反,之前咱们使用的MeshBasicMaterial材料就不反光,所以我们给他换成反光的MeshPhongMaterial材质:

    1
    2
    3
    const material = new THREE.MeshBasicMaterial({color: 0x44aa88});
    改成
    const material = new THREE.MeshPhongMaterial({color: 0x44aa88});

    完整代码如下

    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
    <!DOCTYPE html>
    <html>
    <head>
    <title>threejs响应式设计开发</title>
    <meta charset="utf8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/r99/three.min.js"></script>
    </head>
    <body>
    <canvas id="three"></canvas>
    <script>
    function main () {
    const canvas = document.querySelector('#three')
    const renderer = new THREE.WebGLRenderer({canvas});
    const fov = 75;
    const aspect = 2; // 默认值
    const near = 0.1;
    const far = 5;
    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

    const material = new THREE.MeshPhongMaterial({color: 0x44aa88});

    const boxWidth = 1;
    const boxHeight = 1;
    const boxDepth = 1;
    const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);

    const cube = new THREE.Mesh(geometry, material);
    cube.position.z = -2
    const scene = new THREE.Scene();
    scene.add(cube)

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

    function render (time) {
    time *= 0.001
    cube.rotation.x = time
    cube.rotation.y = time
    renderer.render(scene, camera)
    requestAnimationFrame(render)
    }
    requestAnimationFrame(render)
    }
    window.onload = main
    </script>
    </body>
    </html>

    现在,可以看到立方体转到不同的面时,明暗程度不同。

响应式开发

将html,body的margin设置为0,宽高设置为100%。canvas设置为block,宽高100%。

1
2
3
4
5
6
7
8
9
html,body{
width: 100%;
height: 100%;
}
#three{
display: block;
width: 100%;
height: 100%;
}

这时刷新页面,变换窗口的大小,可以看到两个问题,一个是正方体虚化,边缘有很多锯齿状的东西,第二个是在变化窗口大小的过程中,正方体会变形。接下来我们来解决这两个问题。

解决three.js中边缘锯齿状虚化问题

平时的开发中,我们知道图片有两个尺寸,一个是图片自己的原始尺寸,一个是我们用css设置的图片显示尺寸。如果显示尺寸的大小大于原始图片尺寸,图片就会出现虚化的问题。同理,canvas也是这样,当canvas的原始尺寸小于显示尺寸的时候,就会出现虚化锯齿的问题。

为了解决three.js中物体边缘虚化的问题,我们可以实时对canvas的实际大小和显示大小进行对比,当二者不一致时,我们进行调整就可以。

  • 首先,我们增加监测canvas实际大小和显示大小的函数写出来。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function resizeRenderSizeToDisplaySize (canvas) {
    const displayWidth = canvas.clientWidth;
    const displayHeight = canvas.clientHeight;
    const renderWidth = canvas.width;
    const renderHeight = canvas.height;

    const needResize = displayWidth !== renderWidth || displayHeight !== renderHeight
    return needResize
    }
  • 调整canvas实际大小和显示大小一致,消除锯齿

    使用three.js内置的方法renderer.setSize(width, height,updateStyle)方法。renderer.setSize需要传入三个参数,分别是宽,高和是否根据传入的宽高改变canvas的显示样式。这里我们肯定是不希望它去改变显示样式的,因此第三个值默认传入false就可以了。

    代码如下:

    1
    2
    3
    if (resizeRenderSizeToDisplaySize(canvas)) {
    renderer.setSize(canvas.clientWidth, canvas.clientHeight, false)
    }

    完整代码如下:

    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
    <!DOCTYPE html>
    <html>
    <head>
    <title>threejs响应式设计开发</title>
    <meta charset="utf8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/r99/three.min.js"></script>
    </head>
    <body>
    <canvas id="three"></canvas>
    <script>
    function main () {
    const canvas = document.querySelector('#three')
    const renderer = new THREE.WebGLRenderer({canvas});
    const fov = 75;
    const aspect = 2; // 默认值
    const near = 0.1;
    const far = 5;
    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

    const material = new THREE.MeshPhongMaterial({color: 0x44aa88});

    const boxWidth = 1;
    const boxHeight = 1;
    const boxDepth = 1;
    const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);

    const cube = new THREE.Mesh(geometry, material);
    cube.position.z = -2
    const scene = new THREE.Scene();
    scene.add(cube)

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


    function render (time) {
    if (resizeRenderSizeToDisplaySize(canvas)) {
    render.setSize(canvas.clientWidth, canvas.clientHeight, false)
    }
    time *= 0.001
    cube.rotation.x = time
    cube.rotation.y = time
    renderer.render(scene, camera)
    requestAnimationFrame(render)
    }
    requestAnimationFrame(render)
    }
    function resizeRenderSizeToDisplaySize (canvas) {
    const displayWidth = canvas.clientWidth;
    const displayHeight = canvas.clientHeight;
    const renderWidth = canvas.width;
    const renderHeight = canvas.height;

    const needResize = displayWidth !== renderWidth || displayHeight !== renderHeight
    return needResize
    }
    window.onload = main
    </script>
    </body>
    </html>

这个时候刷新页面,你就能看到锯齿消失了,实体更清晰了。

  • 解决three.js中变形的问题
    在解决锯齿状的问题中,我们是通过改变显示大小和canvas的实际大小来实现的。同理,变形的问题,也是由于实际的宽高比例和显示的宽高比例不同导致的,因此我们只需要在调整宽高比例的时候调整相机显示的宽高比与canvas的宽高比一致即可。如下:
    1
    2
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
    首先我们手动调整aspect为实际canvas的宽高比,接着我们用updateProjectionMatrix()方法告诉three.js我们的相机参数更新了

现在,无论怎们调整浏览器显示窗口大小,立方体都不会变形了。为了节省篇幅,我决定以后不在每一步都贴出代码了,只在每篇结尾将完整代码贴出来,如果需要看完整代码,请转文末。

适配视网膜屏幕

我们知道,在视网膜屏幕中,至少都是2倍或者3倍像素,为了适配视网膜屏幕,我们有两个方法。

  • 使用three自带的renderer.setPixelRatio(pixel)方法将当前设备的像素比传进去,后面的事由three.js自己完成。

代码如下:

1
renderer.setPixelRatio(window.devicePixelRatio);

这样,后面所有的renderer.setSize都会自动乘以传入的window.devicePixelRatio

  • 另一个方法是在调整canvas大小的时候
    首先改变判断实际显示的canvas和canvas实际大小的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function resizeRenderSizeToDisplaySize (canvas) {
    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
    }

    然后改变改变canvas大小的代码

    1
    2
    3
    4
    if (resizeRenderSizeToDisplaySize(canvas)) {
    const pixelRatio = window.devicePixelRatio;
    renderer.setSize(canvas.clientWidth * pixelRatio, canvas.clientHeight * pixelRatio, false)
    }

刷新看一下,可能改变不太大。

最后附上完整代码和我实现的案例

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
<!DOCTYPE html>
<html>
<head>
<title>threejs响应式设计开发</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/r99/three.min.js"></script>
<style>
html,body{
height: 100%;
margin: 0;
}
#three{
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="three"></canvas>
<script>
function main () {
const canvas = document.querySelector('#three')
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2; // 默认值
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

const material = new THREE.MeshPhongMaterial({color: 0x44aa88});

const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);

const cube = new THREE.Mesh(geometry, material);
cube.position.z = -2
const scene = new THREE.Scene();
scene.add(cube)

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


function render (time) {
if (resizeRenderSizeToDisplaySize(canvas)) {
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
cube.rotation.x = time
cube.rotation.y = time
renderer.render(scene, camera)
requestAnimationFrame(render)
}
requestAnimationFrame(render)
}
function resizeRenderSizeToDisplaySize (canvas) {
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
}
window.onload = main
</script>
</body>
</html>