❝目录
❞
本文源码在公众号对话框中回复: 0311 领取。
在本文中,我们将基于 WebGL 与 OGL[1] 来实现一个无限循环画廊。
本文中所用到的大多数套路也可以用在其他 WebGL 库中,例如 Three.js[2] 或 Babylon.js[3] 中,但是需要一些小小的调整。
首先要确保你正确设置了创建 3D 环境所需的所有渲染逻辑。
通常我们需要:一台照相机,一个场景和一个渲染器,它将把所有内容输出到一个 canvas
元素中。然后在 requestAnimationFrame
循环中用相机在渲染器中渲染场景。以下是原始代码段:
App
类的设置在 createRenderer
方法中,通过调用 this.gl.clearColor
来初始化有着固定颜色背景的渲染器。然后将 GL 上下文(this.renderer.gl`)引用存储在 `this.gl` 变量中,并将 `<canvas>
(this.gl.canvas
)元素附加到 document.body
中。
在 createCamera
方法中,我们要创建一个 new Camera()
实例并设置其一些属性:fov
和它的 z
位置。FOV是摄像机的视野,我们通过它来看到最终的画面。z
是相机在 z 轴上的位置。
在 createScene
方法中使用的是 Transform
类,它是一个新场景的表示,包含所有表示 WebGL 环境中图像的平面。
onResize
方法是初始化设置中最重要的部分,负责三件事:
<canvas>
元素的大小。this.camera
透视图,以划分视口的 width
和 height
。this.viewport
存储在变量 this.viewport
中,这个值表示将通过使用摄像机的 fov
将像素转换为 3D 环境尺寸。使用 camera.fov
在 3D 环境尺寸下转换像素的方法在众多的 WebGL 实现中非常常用。基本上它的工作是确保能够执行以下操作:this.mesh.scale.x = this.viewport.width;
这会使我们的网格适合整个屏幕宽度,其表现为 width: 100%
,不过是在 3D 空间中。
最后在更新中,我们设置了 requestAnimationFrame
循环,并确保能够持续渲染场景。
另外代码中还包含了 wheel
、touchstart
、touchmove
、touchend
、mousedown
、mousemove
和 mouseup
事件,它们用于处理用户与我们程序的交互。
不管你用的是哪种 WebGL 库,总是要通过重复使用相同的几何图形引用来保持较低的内存使用量,这是一种很好的做法。为了表示所有图像,我们将使用平面几何图形,所以要创建一个新方法并将新几何图形存储在 this.planeGeometry
变量中。
在这些值中之所以包含 heightSegments
和 widthSegments
,是因为能够通过它们操纵顶点,以使 Plane
的行为像空气中的纸一样。
接下来就要将图像导入我们的程序了。在这里我们使用 Webpack,需要获取图像的操作只需要简单的使用 import
就够了:
现在创建要在轮播滑块中使用的图像数组,并在 createMedia
方法中调用上面的变量。用 .map
创建 Media
类的新实例(new Media()
),它将用来表示画廊程序中每个图片。
你可能注意到了,我们把一堆参数传递给了 Media
类,在下一小节讲到设置类时,会解释为什么需要这样。另外还将复制图片数量,以免在非常宽的屏幕上无限循环时出现图片不足的问题。
在 this.medias
数组的 onResize
和 update
方法中包括一些特定的调用,因为我们希望图像能够响应:
并在 requestAnimationFrame
内部执行一些实时操作:
Media
类Media
类中用 OGL 中的 Mesh
、 Program
和 Texture
类来创建 3D 平面并赋予纹理,在例子中,这个平面会成为我们的图像。
在构造函数中存储所需的所有变量,这些变量是从 index.js
的 new Media()
初始化时传递的:
解释一下其中的参数, geometry
是要应用于 Mesh
类的几何图形。this.gl
是 GL 上下文,用于在类中继续进行 WebGL 操作。this.image
是图像的 URL。this.index
和 this.length
都将用于进行网格的位置计算。this.scene
是要将网格附加到的组。this.screen
和 this.viewport
是视口和环境的大小。
接下来用 createShader
方法创建要应用于 Mesh
的着色器,在 OGL 着色器中是通过 Program
创建的:
在上面的代码段中,创建了一个 new Texture()
实例,并把 generateMipmaps
设置为 false
,以便保留图像的质量。然后创建一个 new Program()
实例,该实例代表由 fragment
和 vertex
组成的着色器,并带有一些用于操纵它的 uniforms
。
代码中将创建了一个 new Image()
实例,用于在 texture.image
之前预加载图像。并且还要更新 this.program.uniforms.uImageSizes.value
,它用于保留图像的长宽比。
现在创建片段和顶点着色器,先创建两个新文件:fragment.glsl
和 vertex.glsl
:
并用 Webpack
在 Media.js
开头中导入它们:
之后在 createMesh
方法中创建 new Mesh()
实例,将几何图形和着色器合并在一起。
把 Mesh
实例存储在 this.plane
变量中,以便在 onResize
和 update
方法中重用,然后作为 this.scene
组的子代附加。
现在屏幕上出现了带有图像的简单正方形:
接着实现 onResize
方法,确保我们能够渲染矩形:
scale.y
和 scale.x
调用负责正确缩放元素,根据缩放比例将先前的正方形转换为 700×900 大小的矩形。
uViewportSizes
和 uPlaneSizes
统一值更新可以使图像正确显示。这就为了使图片具有 background-size: cover;
行为。
现在我们需要在 x 轴上放置所有矩形,确保它们之间有一个很小的间隙。用 this.plane.scale.x
, this.padding
和 this.index
变量来进行移动它们所需的计算:
在 update
方法中将 this.plane.position
设置为以下变量:
现在已经设置好了 Media
的所有初始代码,其结果如下图所示:
现在添加滚动逻辑,所以当用户滚动浏览你的页面时,会有一个无限旋转的画廊。在 index.js
中添加一下代码。
首先在构造函数中包含一个名为 this.scroll
的新对象,其中包含我们将要进行平滑滚动的所有变量:
下面添加触摸和滚轮事件,以便用户与画布交互时他将能够移动东西:
然后在 onWheel
事件中包含 NormalizeWheel
库,这样当用户滚动时,在所有浏览器上能得到有相同的值:
在带有 requestAnimationFrame
的 update
方法中,我们将使用 this.scroll.target对this.scroll.current
进行平滑处理,然后将其传递给所有 media:
现在我们只是更新 Media
文件,用当前滚动值将 Mesh
移到新的滚动位置:
下面是目前的成果:
现在它还不能无限滚动,要实现这一点还需要添加一些代码。第一步是将滚动的方向包含在来自 index.js
的 update
方法中:
在 Media
类的造函数中包含一个名为 this.extra
的变量,并对它进行一些操作,当元素位于屏幕外部时求出图库的总宽度。
现在可以无限滚动了。
首先让它根据位置平滑旋转。map
方法是一种基于另一个特定范围提供值的方法,例如 map(0.5, 0, 1, -500, 500);
将返回 0
,因为它是在 -500
和 500
之间的中间位置。一般来说第一个参数控制 min2
和 max2
的输出:
让我们通过在 Media
类中添加以下类似的代码来观察它的作用:
这是目前的结果。你可以看到旋转根据平面位置而变化:
接下来要让它看起来像圆形。只需要用 Math.cos
给 this.plane.position.x/this.widthTotal
做一个简单的计算即可:
只需根据位置在环境空间中将其移动 75
即可,结果如下所示:
现在添加在用户停止滚动时简单地捕捉到最近的项目。创建一个名为 onCheck
的方法,该方法将在用户释放滚动时进行一些计算:
item
变量的结果始终是图库中元素之一的中心,这会将用户锁定到相应的位置。
对于滚动事件,还需要一个去抖动的版本 onCheckDebounce
,可以通过导入 lodash/debounce
将其添加到构造函数中:
现在画廊总是能够被捕捉到正确的条目:
最后是最有意思的部分,通过滚动速度和使网格的顶点变形来稍微增强着色器。
第一步是在 Media
类的 this.program
声明中包括两个新的 uniform:uSpeed
和 uTime
。
现在编写一些着色器代码,使图像弯曲和变形。在你的 vertex.glsl
文件中,应该添加新的 uniform :uniform float uTime
和 uniform float uSpeed
:
然后在着色器的 void main()
内部,可以用这两个值以及在 p
中存储的 position
变量来操纵 z
轴上的顶点。可以用 sin
和 cos
像平面一样弯曲我们的顶点,添加下面的代码:
同样不要忘记在 Media
的 update()
方法中包含 uTime
增量:
下面是产生的纸张效果动画:
现在把文本用 WebGL 显示出来,首先用 msdf-bmfont
来生成文件,安装 npm
依赖项并运行以下命令:
运行之后,在当前目录中会有一个 .png
和 .json
文件,这些是将在 OGL 中的 MSDF 实现中使用的文件。
创建一个名为 Title
的新文件,在其中创建 class
并在着色器和文件中使用 import
:
现在开始在 createShader()
方法中设置 MSDF 实现代码。首先创建一个新的 Texture()
实例,并加载存储在 src
中的 fonts/freight.png
:
然后设置用于渲染 MSDF 文本的片段着色器,因为可以在 WebGL 2.0 中优化 MSDF,所以使用 OGL 中的 this.renderer.isWebgl2
来检查是否支持,并基于它声明不同的着色器,我们将使用 vertex300
,fragment300
,vertex100
和 fragment100
:
你可能已经注意到,我们在 fragment
和 vertex
之前添加了基于渲染器 WebG L版本的不同设置,接着创建了text-fragment.glsl
和 text-vertex.glsl
文件:
最后在 createMesh()
方法中创建 MSDF 字体实现的几何,使用OGL的 new Text()
实例,然后将由此生成的缓冲区应用于 new Text()
实例:
接下来在 Media
类中应用新的标题,创建一个名为 createTilte()
的新方法,并在 constructor
中调用:
将输出以下结果:
就这个程序而言,我们还实现了一个 new Number()
类,负责显示用户正在查看的当前索引。你可以检查它在源代码中的实现方式,但是它基本上与 Title
类的实现相同,唯一的区别是它加载了不同的字体样式:
最后还需要在后台实现一些将在 x 和 y 轴上移动的块,以增强其深度效果:
为了达到这种效果,需要创建一个新的 Background
类,并在其内部通过更改 scale
来在一个带有随机大小和位置的 new Mesh()
中初始化一些 new Plane()
几何形状。
然后只需要对它们应用无限滚动逻辑,并遵循与 Media
类中相同的方向进行验证:
就这么简单,现在我们的代码终于完成了。
[1]
OGL: https://github.com/oframe/ogl
[2]
Three.js: https://threejs.org/
[3]
Babylon.js: https://www.babylonjs.com/