Skip to content

Commit 52c310e

Browse files
committed
add 27 multiple canvas and screen
1 parent fc91ba7 commit 52c310e

File tree

1 file changed

+386
-0
lines changed

1 file changed

+386
-0
lines changed
Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
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

Comments
 (0)