|
| 1 | +# 27 Three.js解决方案之多画布、多场景 |
| 2 | + |
| 3 | +在我们之前的示例中,通常都是 1 个网页中只有 1 个画布,1 个渲染器,1 个场景。 |
| 4 | + |
| 5 | +1 个画布(Canvas) + 1 个渲染器 相当于在当前浏览器的 JS 中创建了 1 个 webgl。 |
| 6 | + |
| 7 | +> 1 个 webgl 就会占用一定量的内存和性能,浏览器也是为了用户体验着想,所以才会限制 webgl 数量的。 |
| 8 | +
|
| 9 | + |
| 10 | + |
| 11 | +<br> |
| 12 | + |
| 13 | +请注意: |
| 14 | + |
| 15 | +**浏览器并不限制 DOM 中 画布 <canvas \> 标签的数量,浏览器只是限制 webgl 的数量。** |
| 16 | + |
| 17 | +<!-- 关于浏览器不限制画布标签数量这个观点,我并不十分确认是否正确 --> |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +<br> |
| 22 | + |
| 23 | +### 网页中 webgl 数量限制 |
| 24 | + |
| 25 | +不同浏览器都会对 webgl 创建的数量进行限制,通常情况下可以创建 8 个左右。 |
| 26 | + |
| 27 | +如果超出浏览器对 webgl 数量,则新创建的会顶替较早之前创建的。 |
| 28 | + |
| 29 | +> 此时较早之前创建的 webgl 会消失,变为不可用 |
| 30 | +
|
| 31 | + |
| 32 | + |
| 33 | +<br> |
| 34 | + |
| 35 | +如果我们一个网页中需要多个 webgl,那是不是我们多创建几个画布就可以了? |
| 36 | + |
| 37 | +#### 试想一下这个场景 |
| 38 | + |
| 39 | +假设我们现在要制作一个产品列表页,该页面上需要展示 15 个产品,且每一个产品我们都希望搭配一个 3D 模型展示。 |
| 40 | + |
| 41 | +那么我们现在就会遇到一些问题: |
| 42 | + |
| 43 | +1. 问题一:如果每一个产品对应 1 个 webgl,因此我们就需要创建 15 个 webgl,这超出了浏览器对于一个页面上可创建 webgl 数量限制。 |
| 44 | + |
| 45 | + > 我们假设浏览器最多只允许我们创建 8 个 webgl |
| 46 | +
|
| 47 | + > 特别强调:假设我们在一段 JS 代码中创建了 N 个渲染器 或 N 个场景,这并不会创建 N 个webgl,他们仍然被视为仅仅是 1 个 webgl |
| 48 | +
|
| 49 | + > 你可以简单粗暴得去理解:webgl 的数量仅和画布(canvas)数量有关,和创建几个渲染器或场景无关。 |
| 50 | +
|
| 51 | +2. 问题二:假设每个产品只是模型不同,但是所使用的材质相同,或者多个产品使用同一个纹理贴图,如果我们对每一个产品都创建一套 webgl,那同一个材质或贴图就可能需要被我们反复多次加载。换句话说每一个 Three.js 创建的产品 3D 展示都相互独立(孤立),资源无法共享。 |
| 52 | + |
| 53 | + > 上面我们说 “创建一套 webgl” 的意思是:创建一个 canvas,创建 一个渲染器,创建一个场景 等等 |
| 54 | +
|
| 55 | + |
| 56 | + |
| 57 | +<br> |
| 58 | + |
| 59 | +那...解决方案是什么呢? |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +### 第1种解决方案:用其他标签充当占位,然后使用渲染器的剪裁渲染功能 |
| 64 | + |
| 65 | +**用 1 个 画布来渲染全部,用一些其他元素标签来 “代替” “充当” N 个画布。** |
| 66 | + |
| 67 | + |
| 68 | + |
| 69 | +<br> |
| 70 | + |
| 71 | +**具体的事实细节:** |
| 72 | + |
| 73 | +1. 创建一个 <canvas \> 标签,并设置 z-index:-1,这样该画布就会显示在其他元素的下面 |
| 74 | + |
| 75 | + > 事实上相当于将 画布 当成了 “大背景” |
| 76 | +
|
| 77 | +2. 在需要展示 “画布” 的位置,我们添加一些网页标签,用来启到 “占位” 的作用。 |
| 78 | + |
| 79 | + > 该标签里并没有实际内容,但是我们通过 CSS 给该标签添加宽和高 |
| 80 | +
|
| 81 | +3. 在 JS 中使用 Three.js,添加不同的灯光和镜头。 |
| 82 | + |
| 83 | + > 一组灯光和镜头 对应一个 需要渲染的对象内容 |
| 84 | +
|
| 85 | +4. 我们 “判断元素当前是否可见”,然后通过渲染器的以下 3 个方法,对渲染器进行 “裁剪”。 |
| 86 | + |
| 87 | + 1. Renderer.setScissorTest() |
| 88 | + |
| 89 | + > 该方法接收 1 个参数:boolean,来决定是否启用或禁用裁剪检测。 |
| 90 | +
|
| 91 | + 2. Renderer.setViewport() |
| 92 | + |
| 93 | + > 该方法接收 4 个参数:x、y、width、height,这 4 个参数构成 1 个矩形的裁剪框。 |
| 94 | + > |
| 95 | + > 若此时已启用 剪裁检测,那么只有在该矩形框内的才会被渲染,不在该矩形框内的则不会被渲染。 |
| 96 | +
|
| 97 | + 3. Renderer.setScissor() |
| 98 | + |
| 99 | + > 该方法接收 4 个参数:x、y、width、height,这 4 个参数构成 1 个视窗(视框)。 |
| 100 | +
|
| 101 | +5. 不断判断,不断清空画布内容,已实现实时更新裁剪可见区域。 |
| 102 | + |
| 103 | + > 但是请注意:由于 Three.js 渲染需要一定时间,当网页快速滚动时可能会出现 “渲染不及时”,看上去似乎是一个 “bug”,具体我们会稍后讲解。 |
| 104 | +
|
| 105 | +6. 最终,我们将那些 “占位”标签的位置和尺寸 传递给 Three.js,通过 裁剪,只在渲染出相应内容。 |
| 106 | + |
| 107 | + |
| 108 | + |
| 109 | +<br> |
| 110 | + |
| 111 | +下面我们将针对以上步骤中,一些关键的点进行详细讲解。 |
| 112 | + |
| 113 | + |
| 114 | + |
| 115 | +<br> |
| 116 | + |
| 117 | +### 第1:启到占位作用的网页标签 |
| 118 | + |
| 119 | +我们知道这些标签本身不需要显示任何内容,我们会通过 CSS 来给他们设定宽高。 |
| 120 | + |
| 121 | +那究竟使用什么标签呢? |
| 122 | + |
| 123 | +我们会很容易想到 <div \> 、<span \> 这些标签都可以。无论使用哪个标签,我们只要确保这些标签统一即可。 |
| 124 | + |
| 125 | +**我们推荐一种更加优雅、通用、明确的做法:给标签添加 html5 新增的 data-* 属性** |
| 126 | + |
| 127 | + |
| 128 | + |
| 129 | +<br> |
| 130 | + |
| 131 | +**data-* 属性介绍:** |
| 132 | + |
| 133 | +在传统的网页标签中,例如 <span \> 标签,默认它只能有以下几种信息: |
| 134 | + |
| 135 | +1. 该标签拥有的 属性和处理事件函数,例如 id、onclick 等 |
| 136 | +2. 该标签的样式,例如 class、style |
| 137 | +3. 该标签和闭合标签之间的内容 |
| 138 | + |
| 139 | +除此之外,该标签无法承载其他信息。 |
| 140 | + |
| 141 | +> 实际上若想还包含其他信息,通常变相的实现手段是将其他信息 包装成 样式名称(class name) |
| 142 | +
|
| 143 | + |
| 144 | + |
| 145 | +<br> |
| 146 | + |
| 147 | +在 HTML5 出现之后,任何标签都可以新增以 `data-*` 的自定义属性。 |
| 148 | + |
| 149 | +> 请注意上面中的 * 是需要我们自己根据实际情况来自定义的 |
| 150 | +
|
| 151 | +例如我们给 <span /\> 添加一个额外的属性,也就是自定义信息 data-author: |
| 152 | + |
| 153 | +``` |
| 154 | +<span id='myspan' data-author='ypx'></span> |
| 155 | +``` |
| 156 | + |
| 157 | +> 上面代码中,我们给 span 增加了一个自定义属性 data-author,我们假设用这个属性来记录作者名字 |
| 158 | +
|
| 159 | + |
| 160 | + |
| 161 | +<br> |
| 162 | + |
| 163 | +**我们可以通过以下 JS 获取该标签:** |
| 164 | + |
| 165 | +``` |
| 166 | +document.querySelector('#span') |
| 167 | +``` |
| 168 | + |
| 169 | +现在,我们还可以通过查找自定义属性的方式,来获取: |
| 170 | + |
| 171 | +``` |
| 172 | +document.getAttribute('data-author') |
| 173 | +``` |
| 174 | + |
| 175 | +> 如果要获取多个拥有该属性的 DOM 元素,我们可以使用:getAttributes() 这个方法 |
| 176 | +
|
| 177 | + |
| 178 | + |
| 179 | +<br> |
| 180 | + |
| 181 | +**使用 CSS 统一获取并设置样式:** |
| 182 | + |
| 183 | +``` |
| 184 | +span{ |
| 185 | + content:attr(data-author) |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +或 |
| 190 | + |
| 191 | +``` |
| 192 | +span[data-author]{ |
| 193 | + ... |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +甚至直接给所有拥有 data-author 属性的标签统一设置样式 |
| 198 | + |
| 199 | +``` |
| 200 | +[data-author]{ |
| 201 | + ... |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | + |
| 206 | + |
| 207 | +<br> |
| 208 | + |
| 209 | +**补充说明:** |
| 210 | + |
| 211 | +上面讲解的都是我们在 JS 中获取标签的自定义属性,假设要通过 JS 给标签添加自定义属性,还是以 span 为例,具体操作方式为: |
| 212 | + |
| 213 | +第1种方式:使用 setAttribute() |
| 214 | + |
| 215 | +```` |
| 216 | +span.setAttribute('data-author','xxxx') |
| 217 | +```` |
| 218 | + |
| 219 | +<br> |
| 220 | + |
| 221 | +第2种方式:使用 dataset |
| 222 | + |
| 223 | +``` |
| 224 | +span.dataset.author = 'xxxx' |
| 225 | +``` |
| 226 | + |
| 227 | +请注意: |
| 228 | + |
| 229 | +1. dataset 作为该标签的自定义属性统一对象,该标签的所有自定义属性都将挂载在该属性值下面 |
| 230 | + |
| 231 | +2. 我们在去设置自定义属性名时,是无需添加 "data-" 的,例如原本的 data-author 我们只需 dataset.author |
| 232 | + |
| 233 | +3. 自定义属性名需遵循驼峰命名方式,在上面示例中我们自定义属性为 `data-author`,去掉不用写的 data-,那剩下的就只有 author,我们可以直接这样写。但是假设我们自定义属性名为 `data-author-name`,此时去掉不用写的 data- 后,还剩下 author-name,我们就需要遵循驼峰命名方式,实际代码应为: |
| 234 | + |
| 235 | + ``` |
| 236 | + span.dataset.authorName = 'xxx' |
| 237 | + ``` |
| 238 | + |
| 239 | + > 浏览器会自动将驼峰命名转化为 data-xxx-xxx 赋予给标签 |
| 240 | +
|
| 241 | + |
| 242 | + |
| 243 | +<br> |
| 244 | + |
| 245 | +回到我们本文要讲解的内容上面,我们可以将负责 "占位" 的标签都添加上统一的自定义属性,这样在 JS 中可根据该自定义字段来获取所有占位的标签。 |
| 246 | + |
| 247 | +> 这样的做法对于我们来说有一个好处,就是不用再考虑标签究竟使用的是 <div \> 还是 <span \> |
| 248 | +
|
| 249 | + |
| 250 | + |
| 251 | +<br> |
| 252 | + |
| 253 | +### 第2:判断网页中某标签当前是否在可见窗口内,并告知渲染器进行如何裁切渲染 |
| 254 | + |
| 255 | +大体思路为: |
| 256 | + |
| 257 | +1. 在 JS 中获取该标签,假设该标签(DOM元素)在 js 中的变量引用名为 elem |
| 258 | + |
| 259 | +2. 通过 elem.getBoundingClientRect() 获取该标签相对于视窗的位置信息 |
| 260 | + |
| 261 | + > 这些位置信息有:left、right、top、bottom、width、height |
| 262 | +
|
| 263 | +3. 然后进行判断,如果出现以下情况,只要符合一条,那么我们就可以直接认为该标签当前不在可见窗口内。 |
| 264 | + |
| 265 | + ``` |
| 266 | + bottom < 0 |
| 267 | + top > canvas.clientHeight |
| 268 | + right < 0 |
| 269 | + left > canvas.clientWidth |
| 270 | + ``` |
| 271 | + |
| 272 | +4. 假设我们经过判断元素在可见窗口内,那么我们就要告知渲染器可以根据该元素的位置和尺寸,来进行裁剪渲染。 |
| 273 | + |
| 274 | + ``` |
| 275 | + //让画布的高 - 元素的底部,从而计算出超出的部分,这些部分不必再做渲染了 |
| 276 | + const positiveYUpBottom = canvas.clientHeight - bottom |
| 277 | + |
| 278 | + renderer.setScissor(left,positiveYUpBottom,width,height) |
| 279 | + renderer.setViewport(left,positiveYUpBottom,width,height) |
| 280 | + ``` |
| 281 | + |
| 282 | + |
| 283 | + |
| 284 | +<br> |
| 285 | + |
| 286 | +### 第3:添加轨道控制器、将光添加到镜头中,而非场景中 |
| 287 | + |
| 288 | +这里讲解一个新的知识点。 |
| 289 | + |
| 290 | +在以前所有的示例中,假设我们希望物体有反射光,那么我们都会创建光,并将光添加到场景(Three.Scene)中。 |
| 291 | + |
| 292 | +此时我们添加镜头轨道控制器,当移动鼠标修改镜头位置时,光的位置是不变的。 |
| 293 | + |
| 294 | +> 因为我们是将 光 添加到了场景中,所以光的位置是和场景保持固定不变的。 |
| 295 | +
|
| 296 | + |
| 297 | + |
| 298 | +<br> |
| 299 | + |
| 300 | +假设我们的场景中有多个物体,每个物体都有自己对应的镜头,我们希望对每个物体的镜头添加轨道控制器,且保证物体对应的光永远跟随着镜头移动,那么我就要将光添加到镜头里。 |
| 301 | + |
| 302 | + |
| 303 | + |
| 304 | +<br> |
| 305 | + |
| 306 | +你没有听错,我再说一遍:**将光由原来添加到场景中,修改为添加到镜头中。** |
| 307 | + |
| 308 | +```diff |
| 309 | +- scene.add(light) |
| 310 | ++ camera.add(light) |
| 311 | +``` |
| 312 | + |
| 313 | +如此操作之后,光就不再跟随场景,而是跟随着镜头移动而移动。 |
| 314 | + |
| 315 | +这样可以保证我们每个物体的镜头中,始终有该物体的光 |
| 316 | + |
| 317 | + |
| 318 | + |
| 319 | +<br> |
| 320 | + |
| 321 | +**对于本文示例讲解的场景,不推荐使用 OrbitControls,而是推荐使用 TrackballControls。** |
| 322 | + |
| 323 | +> TrackballControls 不提供滚动鼠标中轴缩放镜头这个功能,因为在这个示例场景中,滚动鼠标应该出现的是网页的滚动,而不是 Three.js 场景的视角缩放。 |
| 324 | +
|
| 325 | +请一定记得在每次渲染函数中,要对轨道控制器进行更新: |
| 326 | + |
| 327 | +1. controls.handleResize() |
| 328 | +2. controls.update() |
| 329 | + |
| 330 | + |
| 331 | + |
| 332 | +<br> |
| 333 | + |
| 334 | +### 第2种解决方案:通过 web worker 来创建和渲染场景 |
| 335 | + |
| 336 | +**该方案的优点很明确:** |
| 337 | + |
| 338 | +1. 本身就是对网页性能的一种提升 |
| 339 | +2. 由于是 web worker,不再受限于浏览器对 webgl 数量的限制 |
| 340 | + |
| 341 | + |
| 342 | + |
| 343 | +<br> |
| 344 | + |
| 345 | +**不过缺点也很明确:** |
| 346 | + |
| 347 | +1. 需要浏览器支持 OffscreenCanvas 才可以 |
| 348 | + |
| 349 | + > 目前火狐、苹果浏览器均不支持 OffscreenCanvas |
| 350 | +
|
| 351 | +2. 默认 web worker 内部不支持对 DOM 元素交互事件的侦听,也就是说无法添加 轨道控制器 |
| 352 | + |
| 353 | + > 不过可以通过变相的方式,请参考本系列教程 [22 Three.js优化之OffscreenCanvas与WebWorker.md](https://github.com/puxiao/threejs-tutorial/blob/main/22%20Three.js%E4%BC%98%E5%8C%96%E4%B9%8BOffscreenCanvas%E4%B8%8EWebWorker.md) |
| 354 | +
|
| 355 | + |
| 356 | + |
| 357 | +<br> |
| 358 | + |
| 359 | +### 第3种解决方案:Three.js渲染的画布不直接显示,让不同位置的标签(画布)去复制该画布的局部结果 |
| 360 | + |
| 361 | +由于浏览器并不显示 画布 的数量,我们可以将不同位置的 占位标签 直接使用画布标签,然后让不同的画布去复制渲染出的画布结果内容。 |
| 362 | + |
| 363 | +这样做的缺点是:性能不好,速度慢,每个区域都需要进行相应的复制操作。 |
| 364 | + |
| 365 | +<!-- 关于浏览器不限制画布标签数量这个观点,我并不十分确认是否正确 --> |
| 366 | + |
| 367 | + |
| 368 | + |
| 369 | +<br> |
| 370 | + |
| 371 | +本文只是阐述了某些特殊场景,例如需要多画布、多场景的情况下的解决方案。 |
| 372 | + |
| 373 | +并没有深入、完整编写示例代码。 |
| 374 | + |
| 375 | +> 我个人认出现这种场景的几率并不大,所以就偷懒一下,不去写完整的示例了。 |
| 376 | +
|
| 377 | + |
| 378 | + |
| 379 | +<br> |
| 380 | + |
| 381 | +本文此致结束。 |
| 382 | + |
| 383 | +下一节,我们将讲解一个非常重要的内容,关乎绝大多数我们编写的 Three.js 程序。 |
| 384 | + |
| 385 | +那就是:鼠标选中场景中的物体,并发生交互。 |
| 386 | + |
0 commit comments