27.Three.js解决方案之多画布、多场景

27 Three.js解决方案之多画布、多场景

在我们之前的示例中,通常都是1个网页中只有1个画布,1个渲染器,1个场景。

1个画布(Canvas) + 1个渲染器 相当于在当前浏览器的JS中创建了1webgl

1webgl就会占用一定量的内存和性能,浏览器也是为了用户体验着想,所以才会限制webgl数量的。


请注意:

浏览器并不限制DOM中 画布<canvas >标签的数量,浏览器只是限制webgl的数量。


网页中webgl数量限制

不同浏览器都会对webgl创建的数量进行限制,通常情况下可以创建8个左右。

如果超出浏览器对webgl数量,则新创建的会顶替较早之前创建的。

此时较早之前创建的webgl会消失,变为不可用


如果我们一个网页中需要多个webgl,那是不是我们多创建几个画布就可以了?

试想一下这个场景

假设我们现在要制作一个产品列表页,该页面上需要展示15个产品,且每一个产品我们都希望搭配一个3D模型展示。

那么我们现在就会遇到一些问题:

  1. 问题一:如果每一个产品对应1webgl,因此我们就需要创建15webgl,这超出了浏览器对于一个页面上可创建webgl数量限制。

    我们假设浏览器最多只允许我们创建8webgl

    特别强调:假设我们在一段JS代码中创建了N个渲染器 或N个场景,这并不会创建Nwebgl,他们仍然被视为仅仅是1webgl

    你可以简单粗暴得去理解:webgl的数量仅和画布(canvas)数量有关,和创建几个渲染器或场景无关。

  2. 问题二:假设每个产品只是模型不同,但是所使用的材质相同,或者多个产品使用同一个纹理贴图,如果我们对每一个产品都创建一套webgl,那同一个材质或贴图就可能需要被我们反复多次加载。换句话说每一个Three.js创建的产品3D展示都相互独立(孤立),资源无法共享。

    上面我们说 “创建一套webgl” 的意思是:创建一个canvas,创建 一个渲染器,创建一个场景 等等


那…解决方案是什么呢?

1种解决方案:用其他标签充当占位,然后使用渲染器的剪裁渲染功能

1个 画布来渲染全部,用一些其他元素标签来 “代替” “充当” N个画布。


具体的事实细节:

  1. 创建一个<canvas >标签,并设置z-index:-1,这样该画布就会显示在其他元素的下面

    事实上相当于将 画布 当成了 “大背景”

  2. 在需要展示 “画布” 的位置,我们添加一些网页标签,用来启到 “占位” 的作用。

    该标签里并没有实际内容,但是我们通过CSS给该标签添加宽和高

  3. JS中使用Three.js,添加不同的灯光和镜头。

    一组灯光和镜头 对应一个 需要渲染的对象内容

  4. 我们 “判断元素当前是否可见”,然后通过渲染器的以下3个方法,对渲染器进行 “裁剪”。

    1. Renderer.setScissorTest()

      该方法接收1个参数:boolean,来决定是否启用或禁用裁剪检测。

    2. Renderer.setViewport()

      该方法接收4个参数:x、y、width、height,这4个参数构成1个矩形的裁剪框。

      若此时已启用 剪裁检测,那么只有在该矩形框内的才会被渲染,不在该矩形框内的则不会被渲染。

    3. Renderer.setScissor()

      该方法接收4个参数:x、y、width、height,这4个参数构成1个视窗(视框)

  5. 不断判断,不断清空画布内容,已实现实时更新裁剪可见区域。

    但是请注意:由于Three.js渲染需要一定时间,当网页快速滚动时可能会出现 “渲染不及时”,看上去似乎是一个 “bug”,具体我们会稍后讲解。

  6. 最终,我们将那些 “占位”标签的位置和尺寸 传递给Three.js,通过 裁剪,只在渲染出相应内容。


下面我们将针对以上步骤中,一些关键的点进行详细讲解。


1:启到占位作用的网页标签

我们知道这些标签本身不需要显示任何内容,我们会通过CSS来给他们设定宽高。

那究竟使用什么标签呢?

我们会很容易想到<div ><span >这些标签都可以。无论使用哪个标签,我们只要确保这些标签统一即可。

我们推荐一种更加优雅、通用、明确的做法:给标签添加html5新增的data-*属性


data-*属性介绍:

在传统的网页标签中,例如<span >标签,默认它只能有以下几种信息:

  1. 该标签拥有的 属性和处理事件函数,例如idonclick
  2. 该标签的样式,例如class、style
  3. 该标签和闭合标签之间的内容

除此之外,该标签无法承载其他信息。

实际上若想还包含其他信息,通常变相的实现手段是将其他信息 包装成 样式名称(class name)


HTML5出现之后,任何标签都可以新增以 data-* 的自定义属性。

请注意上面中的*是需要我们自己根据实际情况来自定义的

例如我们给<span />添加一个额外的属性,也就是自定义信息data-author

<span id='myspan' data-author='ypx'></span>

上面代码中,我们给span增加了一个自定义属性data-author,我们假设用这个属性来记录作者名字


我们可以通过以下JS获取该标签:

document.querySelector('#span')

现在,我们还可以通过查找自定义属性的方式,来获取:

document.getAttribute('data-author')

如果要获取多个拥有该属性的DOM元素,我们可以使用:getAttributes()这个方法


使用CSS统一获取并设置样式:

span{
	content:attr(data-author)
}

span[data-author]{
	...
}

甚至直接给所有拥有data-author属性的标签统一设置样式

[data-author]{
	...
}

补充说明:

上面讲解的都是我们在JS中获取标签的自定义属性,假设要通过JS给标签添加自定义属性,还是以span为例,具体操作方式为:

1种方式:使用setAttribute()

span.setAttribute('data-author','xxxx')

2种方式:使用dataset

span.dataset.author = 'xxxx'

请注意:

  1. dataset作为该标签的自定义属性统一对象,该标签的所有自定义属性都将挂载在该属性值下面

  2. 我们在去设置自定义属性名时,是无需添加 “data-” 的,例如原本的data-author我们只需dataset.author

  3. 自定义属性名需遵循驼峰命名方式,在上面示例中我们自定义属性为 data-author,去掉不用写的data-,那剩下的就只有author,我们可以直接这样写。但是假设我们自定义属性名为 data-author-name,此时去掉不用写的data-后,还剩下author-name,我们就需要遵循驼峰命名方式,实际代码应为:

    span.dataset.authorName = 'xxx'
    

    浏览器会自动将驼峰命名转化为data-xxx-xxx赋予给标签


回到我们本文要讲解的内容上面,我们可以将负责 “占位” 的标签都添加上统一的自定义属性,这样在JS中可根据该自定义字段来获取所有占位的标签。

这样的做法对于我们来说有一个好处,就是不用再考虑标签究竟使用的是<div >还是<span >


2:判断网页中某标签当前是否在可见窗口内,并告知渲染器进行如何裁切渲染

大体思路为:

  1. JS中获取该标签,假设该标签(DOM元素)js中的变量引用名为elem

  2. 通过elem.getBoundingClientRect()获取该标签相对于视窗的位置信息

    这些位置信息有:left、right、top、bottom、width、height

  3. 然后进行判断,如果出现以下情况,只要符合一条,那么我们就可以直接认为该标签当前不在可见窗口内。

    bottom < 0
    top > canvas.clientHeight
    right < 0
    left > canvas.clientWidth
    
  4. 假设我们经过判断元素在可见窗口内,那么我们就要告知渲染器可以根据该元素的位置和尺寸,来进行裁剪渲染。

    //让画布的高 - 元素的底部,从而计算出超出的部分,这些部分不必再做渲染了
    const positiveYUpBottom = canvas.clientHeight - bottom
    
    renderer.setScissor(left,positiveYUpBottom,width,height)
    renderer.setViewport(left,positiveYUpBottom,width,height)
    

3:添加轨道控制器、将光添加到镜头中,而非场景中

这里讲解一个新的知识点。

在以前所有的示例中,假设我们希望物体有反射光,那么我们都会创建光,并将光添加到场景(Three.Scene)中。

此时我们添加镜头轨道控制器,当移动鼠标修改镜头位置时,光的位置是不变的。

因为我们是将 光 添加到了场景中,所以光的位置是和场景保持固定不变的。


假设我们的场景中有多个物体,每个物体都有自己对应的镜头,我们希望对每个物体的镜头添加轨道控制器,且保证物体对应的光永远跟随着镜头移动,那么我就要将光添加到镜头里。


你没有听错,我再说一遍:将光由原来添加到场景中,修改为添加到镜头中。

-	scene.add(light)
+	camera.add(light)

如此操作之后,光就不再跟随场景,而是跟随着镜头移动而移动。

这样可以保证我们每个物体的镜头中,始终有该物体的光


对于本文示例讲解的场景,不推荐使用OrbitControls,而是推荐使用TrackballControls

TrackballControls不提供滚动鼠标中轴缩放镜头这个功能,因为在这个示例场景中,滚动鼠标应该出现的是网页的滚动,而不是Three.js场景的视角缩放。

请一定记得在每次渲染函数中,要对轨道控制器进行更新:

  1. controls.handleResize()
  2. controls.update()

2种解决方案:通过web worker来创建和渲染场景

该方案的优点很明确:

  1. 本身就是对网页性能的一种提升
  2. 由于是web worker,不再受限于浏览器对webgl数量的限制

不过缺点也很明确:

  1. 需要浏览器支持OffscreenCanvas才可以

    目前火狐、苹果浏览器均不支持OffscreenCanvas

  2. 默认web worker内部不支持对DOM元素交互事件的侦听,也就是说无法添加 轨道控制器

    不过可以通过变相的方式,请参考本系列教程 22 Three.js优化之OffscreenCanvasWebWorker.md


3种解决方案:Three.js渲染的画布不直接显示,让不同位置的标签(画布)去复制该画布的局部结果

由于浏览器并不显示 画布 的数量,我们可以将不同位置的 占位标签 直接使用画布标签,然后让不同的画布去复制渲染出的画布结果内容。

这样做的缺点是:性能不好,速度慢,每个区域都需要进行相应的复制操作。


本文只是阐述了某些特殊场景,例如需要多画布、多场景的情况下的解决方案。

并没有深入、完整编写示例代码。

我个人认出现这种场景的几率并不大,所以就偷懒一下,不去写完整的示例了。


本文此致结束。

下一节,我们将讲解一个非常重要的内容,关乎绝大多数我们编写的Three.js程序。

那就是:鼠标选中场景中的物体,并发生交互。

上一页
下一页