vlambda博客
学习文章列表

【实战】用 WebGL 创建一个在线画廊

// 每日前端夜话 第498篇
// 正文共:6000 字
// 预计阅读时间:15 分钟


目录

  • 创建 OGL 3D 环境

  • 解释 `App` 类的设置

  • 创建可重用的几何实例

  • 用 Webpack 导入图像

  • 设置 `Media` 类

  • 添加无限滚动逻辑

  • 加入圆周旋转

  • 捕捉到最接近的项目

  • 编写着色器

  • 用MSDF字体在WebGL中包含文本

  • 引入背景块

在本文中,我们将基于 WebGL 与 OGL[1] 来实现一个无限循环画廊。

本文中所用到的大多数套路也可以用在其他 WebGL 库中,例如 Three.js[2]Babylon.js[3] 中,但是需要一些小小的调整。

创建 OGL 3D 环境

首先要确保你正确设置了创建 3D 环境所需的所有渲染逻辑。

通常我们需要:一台照相机,一个场景和一个渲染器,它将把所有内容输出到一个 canvas 元素中。然后在 requestAnimationFrame 循环中用相机在渲染器中渲染场景。以下是原始代码段:

import { Renderer, Camera, Transform } from 'ogl'
 
export default class App {
  constructor () {
    this.createRenderer()
    this.createCamera()
    this.createScene()
 
    this.onResize()
 
    this.update()
 
    this.addEventListeners()
  }
 
  createRenderer () {
    this.renderer = new Renderer()
 
    this.gl = this.renderer.gl
    this.gl.clearColor(0.796078431370.792156862740.741176470581)
 
    document.body.appendChild(this.gl.canvas)
  }
 
  createCamera () {
    this.camera = new Camera(this.gl)
    this.camera.fov = 45
    this.camera.position.z = 20
  }
 
  createScene () {
    this.scene = new Transform()
  }
 
  /**
   * Events.
   */

  onTouchDown (event) {
      
  }
 
  onTouchMove (event) {
      
  }
 
  onTouchUp (event) {
      
  }
 
  onWheel (event) {
      
  }
 
  /**
   * Resize.
   */

  onResize () {
    this.screen = {
      heightwindow.innerHeight,
      widthwindow.innerWidth
    }
 
    this.renderer.setSize(this.screen.width, this.screen.height)
 
    this.camera.perspective({
      aspectthis.gl.canvas.width / this.gl.canvas.height
    })
 
    const fov = this.camera.fov * (Math.PI / 180)
    const height = 2 * Math.tan(fov / 2) * this.camera.position.z
    const width = height * this.camera.aspect
 
    this.viewport = {
      height,
      width
    }
  }
 
  /**
   * Update.
   */

  update () {
    this.renderer.render({
      scenethis.scene,
      camerathis.camera
    })
    
    window.requestAnimationFrame(this.update.bind(this))
  }
 
  /**
   * Listeners.
   */

  addEventListeners () {
    window.addEventListener('resize'this.onResize.bind(this))
 
    window.addEventListener('mousewheel'this.onWheel.bind(this))
    window.addEventListener('wheel'this.onWheel.bind(this))
 
    window.addEventListener('mousedown'this.onTouchDown.bind(this))
    window.addEventListener('mousemove'this.onTouchMove.bind(this))
    window.addEventListener('mouseup'this.onTouchUp.bind(this))
 
    window.addEventListener('touchstart'this.onTouchDown.bind(this))
    window.addEventListener('touchmove'this.onTouchMove.bind(this))
    window.addEventListener('touchend'this.onTouchUp.bind(this))
  }
}
 
new App()

解释 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 方法是初始化设置中最重要的部分,负责三件事:

  1. 确保我们能够始终用正确的视口大小调整 <canvas> 元素的大小。
  2. 更新 this.camera 透视图,以划分视口的 widthheight
  3. 将变量值 this.viewport 存储在变量 this.viewport 中,这个值表示将通过使用摄像机的 fov 将像素转换为 3D 环境尺寸。

使用 camera.fov 在 3D 环境尺寸下转换像素的方法在众多的 WebGL 实现中非常常用。基本上它的工作是确保能够执行以下操作:this.mesh.scale.x = this.viewport.width; 这会使我们的网格适合整个屏幕宽度,其表现为 width: 100% ,不过是在 3D 空间中。

最后在更新中,我们设置了 requestAnimationFrame 循环,并确保能够持续渲染场景。

另外代码中还包含了 wheeltouchstarttouchmovetouchendmousedownmousemovemouseup 事件,它们用于处理用户与我们程序的交互。

创建可重用的几何实例

不管你用的是哪种 WebGL 库,总是要通过重复使用相同的几何图形引用来保持较低的内存使用量,这是一种很好的做法。为了表示所有图像,我们将使用平面几何图形,所以要创建一个新方法并将新几何图形存储在 this.planeGeometry 变量中。

import { Renderer, Camera, Transform, Plane } from 'ogl'
 
createGeometry () {
  this.planeGeometry = new Plane(this.gl, {
    heightSegments50,
    widthSegments100
  })
}

在这些值中之所以包含 heightSegmentswidthSegments ,是因为能够通过它们操纵顶点,以使 Plane 的行为像空气中的纸一样。

用 Webpack 导入图像

接下来就要将图像导入我们的程序了。在这里我们使用 Webpack,需要获取图像的操作只需要简单的使用 import 就够了:

import Image1 from 'images/1.jpg'
import Image2 from 'images/2.jpg'
import Image3 from 'images/3.jpg'
import Image4 from 'images/4.jpg'
import Image5 from 'images/5.jpg'
import Image6 from 'images/6.jpg'
import Image7 from 'images/7.jpg'
import Image8 from 'images/8.jpg'
import Image9 from 'images/9.jpg'
import Image10 from 'images/10.jpg'
import Image11 from 'images/11.jpg'
import Image12 from 'images/12.jpg'

现在创建要在轮播滑块中使用的图像数组,并在 createMedia 方法中调用上面的变量。用 .map  创建 Media 类的新实例(new Media()),它将用来表示画廊程序中每个图片。

createMedias () {
  this.mediasImages = [
    { image: Image1, text'New Synagogue' },
    { image: Image2, text'Paro Taktsang' },
    { image: Image3, text'Petra' },
    { image: Image4, text'Gooderham Building' },
    { image: Image5, text'Catherine Palace' },
    { image: Image6, text'Sheikh Zayed Mosque' },
    { image: Image7, text'Madonna Corona' },
    { image: Image8, text'Plaza de Espana' },
    { image: Image9, text'Saint Martin' },
    { image: Image10, text'Tugela Falls' },
    { image: Image11, text'Sintra-Cascais' },
    { image: Image12, text'The Prophet\'s Mosque' },
    { image: Image1, text'New Synagogue' },
    { image: Image2, text'Paro Taktsang' },
    { image: Image3, text'Petra' },
    { image: Image4, text'Gooderham Building' },
    { image: Image5, text'Catherine Palace' },
    { image: Image6, text'Sheikh Zayed Mosque' },
    { image: Image7, text'Madonna Corona' },
    { image: Image8, text'Plaza de Espana' },
    { image: Image9, text'Saint Martin' },
    { image: Image10, text'Tugela Falls' },
    { image: Image11, text'Sintra-Cascais' },
    { image: Image12, text'The Prophet\'s Mosque' },
  ]
 
 
  this.medias = this.mediasImages.map(({ image, text }, index) => {
    const media = new Media({
      geometrythis.planeGeometry,
      glthis.gl,
      image,
      index,
      lengththis.mediasImages.length,
      scenethis.scene,
      screenthis.screen,
      text,
      viewportthis.viewport
    })
 
    return media
  })
}

你可能注意到了,我们把一堆参数传递给了 Media 类,在下一小节讲到设置类时,会解释为什么需要这样。另外还将复制图片数量,以免在非常宽的屏幕上无限循环时出现图片不足的问题。

在  this.medias  数组的 onResizeupdate 方法中包括一些特定的调用,因为我们希望图像能够响应:

onResize () {
  if (this.medias) {
    this.medias.forEach(media => media.onResize({
      screenthis.screen,
      viewportthis.viewport
    }))
  }
}

并在 requestAnimationFrame 内部执行一些实时操作:

update () {
  this.medias.forEach(media => media.update(this.scroll, this.direction))
}

设置 Media

Media 类中用 OGL 中的  MeshProgramTexture 类来创建 3D 平面并赋予纹理,在例子中,这个平面会成为我们的图像。

在构造函数中存储所需的所有变量,这些变量是从 index.jsnew Media() 初始化时传递的:

export default class {
  constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
    this.geometry = geometry
    this.gl = gl
    this.image = image
    this.index = index
    this.length = length
    this.scene = scene
    this.screen = screen
    this.text = text
    this.viewport = viewport
 
    this.createShader()
    this.createMesh()
 
    this.onResize()
  }
}

解释一下其中的参数, geometry 是要应用于 Mesh 类的几何图形。this.gl 是 GL 上下文,用于在类中继续进行 WebGL 操作。this.image 是图像的 URL。this.indexthis.length 都将用于进行网格的位置计算。this.scene 是要将网格附加到的组。this.screenthis.viewport 是视口和环境的大小。

接下来用 createShader 方法创建要应用于 Mesh 的着色器,在 OGL 着色器中是通过 Program 创建的:

createShader () {
  const texture = new Texture(this.gl, {
    generateMipmapsfalse
  })
 
  this.program = new Program(this.gl, {
    fragment,
    vertex,
    uniforms: {
      tMap: { value: texture },
      uPlaneSizes: { value: [00] },
      uImageSizes: { value: [00] },
      uViewportSizes: { value: [this.viewport.width, this.viewport.height] }
      },
    transparenttrue
  })
 
  const image = new Image()
 
  image.src = this.image
  image.onload = _ => {
    texture.image = image
 
    this.program.uniforms.uImageSizes.value = [image.naturalWidth, image.naturalHeight]
  }
}

在上面的代码段中,创建了一个 new Texture() 实例,并把 generateMipmaps 设置为 false ,以便保留图像的质量。然后创建一个 new Program() 实例,该实例代表由 fragmentvertex组成的着色器,并带有一些用于操纵它的 uniforms

代码中将创建了一个 new Image() 实例,用于在 texture.image 之前预加载图像。并且还要更新 this.program.uniforms.uImageSizes.value,它用于保留图像的长宽比。

现在创建片段和顶点着色器,先创建两个新文件:fragment.glslvertex.glsl

precision highp float;
 
uniform vec2 uImageSizes;
uniform vec2 uPlaneSizes;
uniform sampler2D tMap;
 
varying vec2 vUv;
 
void main() {
  vec2 ratio = vec2(
    min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
    min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
  );
 
  vec2 uv = vec2(
    vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
    vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
  );
 
  gl_FragColor.rgb = texture2D(tMap, uv).rgb;
  gl_FragColor.a = 1.0;
}
precision highp float;
 
attribute vec3 position;
attribute vec2 uv;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
varying vec2 vUv;
 
void main() {
  vUv = uv;
 
  vec3 p = position;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

并用 WebpackMedia.js 开头中导入它们:

import fragment from './fragment.glsl'
import vertex from './vertex.glsl'

之后在 createMesh 方法中创建 new Mesh() 实例,将几何图形和着色器合并在一起。

createMesh () {
  this.plane = new Mesh(this.gl, {
    geometrythis.geometry,
    programthis.program
  })
 
  this.plane.setParent(this.scene)
}

Mesh 实例存储在 this.plane 变量中,以便在 onResizeupdate 方法中重用,然后作为 this.scene 组的子代附加。

现在屏幕上出现了带有图像的简单正方形:

接着实现 onResize 方法,确保我们能够渲染矩形:

onResize ({ screen, viewport } = {}) {
  if (screen) {
    this.screen = screen
  }
 
  if (viewport) {
    this.viewport = viewport
 
    this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height]
  }
 
  this.scale = this.screen.height / 1500
 
  this.plane.scale.y = this.viewport.height * (900 * this.scale) / this.screen.height
  this.plane.scale.x = this.viewport.width * (700 * this.scale) / this.screen.width
 
  this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y]
}

scale.yscale.x 调用负责正确缩放元素,根据缩放比例将先前的正方形转换为 700×900 大小的矩形。

uViewportSizesuPlaneSizes 统一值更新可以使图像正确显示。这就为了使图片具有  background-size: cover; 行为。

【实战】用 WebGL 创建一个在线画廊

现在我们需要在 x 轴上放置所有矩形,确保它们之间有一个很小的间隙。用 this.plane.scale.xthis.paddingthis.index 变量来进行移动它们所需的计算:

this.padding = 2
 
this.width = this.plane.scale.x + this.padding
this.widthTotal = this.width * this.length
 
this.x = this.width * this.index

update 方法中将 this.plane.position 设置为以下变量:

update () {
  this.plane.position.x = this.x
}

现在已经设置好了 Media 的所有初始代码,其结果如下图所示:

【实战】用 WebGL 创建一个在线画廊

添加无限滚动逻辑

现在添加滚动逻辑,所以当用户滚动浏览你的页面时,会有一个无限旋转的画廊。在 index.js 中添加一下代码。

首先在构造函数中包含一个名为 this.scroll 的新对象,其中包含我们将要进行平滑滚动的所有变量:

this.scroll = {
  ease0.05,
  current0,
  target0,
  last0
}

下面添加触摸和滚轮事件,以便用户与画布交互时他将能够移动东西:

onTouchDown (event) {
  this.isDown = true
 
  this.scroll.position = this.scroll.current
  this.start = event.touches ? event.touches[0].clientX : event.clientX
}
 
onTouchMove (event) {
  if (!this.isDown) return
 
  const x = event.touches ? event.touches[0].clientX : event.clientX
  const distance = (this.start - x) * 0.01
 
  this.scroll.target = this.scroll.position + distance
}
 
onTouchUp (event) {
  this.isDown = false
}

然后在 onWheel 事件中包含 NormalizeWheel 库,这样当用户滚动时,在所有浏览器上能得到有相同的值:

onWheel (event) {
  const normalized = NormalizeWheel(event)
  const speed = normalized.pixelY
 
  this.scroll.target += speed * 0.005
}

在带有 requestAnimationFrameupdate 方法中,我们将使用 this.scroll.target对this.scroll.current 进行平滑处理,然后将其传递给所有 media:

update () {
  this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease)
 
  if (this.medias) {
    this.medias.forEach(media => media.update(this.scroll))
  }
 
  this.scroll.last = this.scroll.current
 
  window.requestAnimationFrame(this.update.bind(this))
}

现在我们只是更新 Media 文件,用当前滚动值将 Mesh 移到新的滚动位置:

update (scroll) {
  this.plane.position.x = this.x - scroll.current * 0.1
}

下面是目前的成果:

【实战】用 WebGL 创建一个在线画廊

现在它还不能无限滚动,要实现这一点还需要添加一些代码。第一步是将滚动的方向包含在来自 index.jsupdate方法中:

update () {
  this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease)
 
  if (this.scroll.current > this.scroll.last) {
    this.direction = 'right'
  } else {
    this.direction = 'left'
  }
 
  if (this.medias) {
    this.medias.forEach(media => media.update(this.scroll, this.direction))
  }
 
  this.scroll.last = this.scroll.current
}

Media 类的造函数中包含一个名为 this.extra 的变量,并对它进行一些操作,当元素位于屏幕外部时求出图库的总宽度。

constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
  this.extra = 0
}

update (scroll) {
  this.plane.position.x = this.x - scroll.current * 0.1 - this.extra
    
  const planeOffset = this.plane.scale.x / 2
  const viewportOffset = this.viewport.width
 
  this.isBefore = this.plane.position.x + planeOffset < -viewportOffset
  this.isAfter = this.plane.position.x - planeOffset > viewportOffset
 
  if (direction === 'right' && this.isBefore) {
    this.extra -= this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
 
  if (direction === 'left' && this.isAfter) {
    this.extra += this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
}

现在可以无限滚动了。

【实战】用 WebGL 创建一个在线画廊

加入圆周旋转

首先让它根据位置平滑旋转。map 方法是一种基于另一个特定范围提供值的方法,例如 map(0.5, 0, 1, -500, 500); 将返回 0,因为它是在 -500500 之间的中间位置。一般来说第一个参数控制 min2max2 的输出:

export function map (num, min1, max1, min2, max2, round = false{
  const num1 = (num - min1) / (max1 - min1)
  const num2 = (num1 * (max2 - min2)) + min2
 
  if (round) return Math.round(num2)
 
  return num2
}

让我们通过在 Media 类中添加以下类似的代码来观察它的作用:

this.plane.rotation.z = map(this.plane.position.x, -this.widthTotal, this.widthTotal, Math.PI, -Math.PI)

这是目前的结果。你可以看到旋转根据平面位置而变化:

【实战】用 WebGL 创建一个在线画廊

接下来要让它看起来像圆形。只需要用 Math.costhis.plane.position.x/this.widthTotal 做一个简单的计算即可:

this.plane.position.y = Math.cos((this.plane.position.x / this.widthTotal) * Math.PI) * 75 - 75

只需根据位置在环境空间中将其移动 75 即可,结果如下所示:

【实战】用 WebGL 创建一个在线画廊


捕捉到最接近的项目

现在添加在用户停止滚动时简单地捕捉到最近的项目。创建一个名为 onCheck 的方法,该方法将在用户释放滚动时进行一些计算:

onCheck () {
  const { width } = this.medias[0]
  const itemIndex = Math.round(Math.abs(this.scroll.target) / width)
  const item = width * itemIndex
 
  if (this.scroll.target < 0) {
    this.scroll.target = -item
  } else {
    this.scroll.target = item
  }
}

item 变量的结果始终是图库中元素之一的中心,这会将用户锁定到相应的位置。

对于滚动事件,还需要一个去抖动的版本 onCheckDebounce ,可以通过导入 lodash/debounce 将其添加到构造函数中:

import debounce from 'lodash/debounce'
 
constructor ({ camera, color, gl, renderer, scene, screen, url, viewport }) {
  this.onCheckDebounce = debounce(this.onCheck, 200)
}
 
onWheel (event) {
  this.onCheckDebounce()
}

现在画廊总是能够被捕捉到正确的条目:

【实战】用 WebGL 创建一个在线画廊


编写着色器

最后是最有意思的部分,通过滚动速度和使网格的顶点变形来稍微增强着色器。

第一步是在 Media 类的 this.program 声明中包括两个新的 uniform:uSpeeduTime

this.program = new Program(this.gl, {
  fragment,
  vertex,
  uniforms: {
    tMap: { value: texture },
    uPlaneSizes: { value: [00] },
    uImageSizes: { value: [00] },
    uViewportSizes: { value: [this.viewport.width, this.viewport.height] },
    uSpeed: { value0 },
    uTime: { value0 }
  },
  transparenttrue
})

现在编写一些着色器代码,使图像弯曲和变形。在你的 vertex.glsl 文件中,应该添加新的 uniform :uniform float uTimeuniform float uSpeed

uniform float uTime;
uniform float uSpeed;

然后在着色器的 void main()  内部,可以用这两个值以及在 p 中存储的 position 变量来操纵 z 轴上的顶点。可以用 sincos 像平面一样弯曲我们的顶点,添加下面的代码:

p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5);

同样不要忘记在 Media 的  update()  方法中包含 uTime 增量:

this.program.uniforms.uTime.value += 0.04

下面是产生的纸张效果动画:

【实战】用 WebGL 创建一个在线画廊

用MSDF字体在WebGL中包含文本

现在把文本用 WebGL 显示出来,首先用 msdf-bmfont 来生成文件,安装 npm 依赖项并运行以下命令:

msdf-bmfont -f json -m 1024,1024 -d 4 --pot --smart-size freight.otf

运行之后,在当前目录中会有一个 .png.json 文件,这些是将在 OGL 中的 MSDF 实现中使用的文件。

创建一个名为 Title 的新文件,在其中创建 class 并在着色器和文件中使用 import

import AutoBind from 'auto-bind'
import { Color, Geometry, Mesh, Program, Text, Texture } from 'ogl'
 
import fragment from 'shaders/text-fragment.glsl'
import vertex from 'shaders/text-vertex.glsl'
 
import font from 'fonts/freight.json'
import src from 'fonts/freight.png'
 
export default class {
  constructor ({ gl, plane, renderer, text }) {
    AutoBind(this)
 
    this.gl = gl
    this.plane = plane
    this.renderer = renderer
    this.text = text
 
    this.createShader()
    this.createMesh()
  }
}

现在开始在 createShader() 方法中设置 MSDF 实现代码。首先创建一个新的 Texture() 实例,并加载存储在 src 中的 fonts/freight.png

createShader () {
  const texture = new Texture(this.gl, { generateMipmaps: false })
  const textureImage = new Image()
 
  textureImage.src = src
  textureImage.onload = _ => texture.image = textureImage
}

然后设置用于渲染 MSDF 文本的片段着色器,因为可以在 WebGL 2.0 中优化 MSDF,所以使用 OGL 中的 this.renderer.isWebgl2 来检查是否支持,并基于它声明不同的着色器,我们将使用 vertex300fragment300vertex100fragment100

createShader () {
  const vertex100 = `${vertex}`
 
  const fragment100 = `
    #extension GL_OES_standard_derivatives : enable
 
    precision highp float;
 
    ${fragment}
  `

 
  const vertex300 = `#version 300 es
 
    #define attribute in
    #define varying out
 
    ${vertex}
  `

 
  const fragment300 = `#version 300 es
 
    precision highp float;
 
    #define varying in
    #define texture2D texture
    #define gl_FragColor FragColor
 
    out vec4 FragColor;
 
    ${fragment}
  `

 
  let fragmentShader = fragment100
  let vertexShader = vertex100
 
  if (this.renderer.isWebgl2) {
    fragmentShader = fragment300
    vertexShader = vertex300
  }
 
  this.program = new Program(this.gl, {
    cullFacenull,
    depthTestfalse,
    depthWritefalse,
    transparenttrue,
    fragment: fragmentShader,
    vertex: vertexShader,
    uniforms: {
      uColor: { valuenew Color('#545050') },
      tMap: { value: texture }
    }
  })
}

你可能已经注意到,我们在 fragmentvertex 之前添加了基于渲染器 WebG L版本的不同设置,接着创建了text-fragment.glsltext-vertex.glsl 文件:

uniform vec3 uColor;
uniform sampler2D tMap;
 
varying vec2 vUv;
 
void main() {
  vec3 color = texture2D(tMap, vUv).rgb;
 
  float signed = max(min(color.r, color.g), min(max(color.r, color.g), color.b)) - 0.5;
  float d = fwidth(signed);
  float alpha = smoothstep(-d, d, signed);
 
  if (alpha < 0.02) discard;
 
  gl_FragColor = vec4(uColor, alpha);
}
attribute vec2 uv;
attribute vec3 position;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
varying vec2 vUv;
 
void main() {
  vUv = uv;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

最后在 createMesh() 方法中创建 MSDF 字体实现的几何,使用OGL的 new Text() 实例,然后将由此生成的缓冲区应用于 new Text() 实例:

createMesh () {
  const text = new Text({
    align'center',
    font,
    letterSpacing-0.05,
    size0.08,
    textthis.text,
    wordSpacing0,
  })
 
  const geometry = new Geometry(this.gl, {
    position: { size3data: text.buffers.position },
    uv: { size2data: text.buffers.uv },
    id: { size1data: text.buffers.id },
    index: { data: text.buffers.index }
  })
 
  geometry.computeBoundingBox()
 
  this.mesh = new Mesh(this.gl, { geometry, programthis.program })
  this.mesh.position.y = -this.plane.scale.y * 0.5 - 0.085
  this.mesh.setParent(this.plane)
}

接下来在 Media 类中应用新的标题,创建一个名为 createTilte() 的新方法,并在 constructor 中调用:

constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
  this.createTitle()
}

createTitle () {
  this.title = new Title({
    glthis.gl,
    planethis.plane,
    rendererthis.renderer,
    textthis.text,
  })
}

将输出以下结果:

【实战】用 WebGL 创建一个在线画廊

就这个程序而言,我们还实现了一个 new Number() 类,负责显示用户正在查看的当前索引。你可以检查它在源代码中的实现方式,但是它基本上与 Title 类的实现相同,唯一的区别是它加载了不同的字体样式:

【实战】用 WebGL 创建一个在线画廊

引入背景块

最后还需要在后台实现一些将在 x 和 y 轴上移动的块,以增强其深度效果:

【实战】用 WebGL 创建一个在线画廊

为了达到这种效果,需要创建一个新的 Background 类,并在其内部通过更改 scale 来在一个带有随机大小和位置的 new Mesh() 中初始化一些 new Plane() 几何形状。

import { Color, Mesh, Plane, Program } from 'ogl'
 
import fragment from 'shaders/background-fragment.glsl'
import vertex from 'shaders/background-vertex.glsl'
 
import { random } from 'utils/math'
 
export default class {
  constructor ({ gl, scene, viewport }) {
    this.gl = gl
    this.scene = scene
    this.viewport = viewport
 
    const geometry = new Plane(this.gl)
    const program = new Program(this.gl, {
      vertex,
      fragment,
      uniforms: {
        uColor: { valuenew Color('#c4c3b6') }
      },
      transparenttrue
    })
 
    this.meshes = []
 
    for (let i = 0; i < 50; i++) {
      let mesh = new Mesh(this.gl, {
        geometry,
        program,
      })
 
      const scale = random(0.751)
 
      mesh.scale.x = 1.6 * scale
      mesh.scale.y = 0.9 * scale
 
      mesh.speed = random(0.751)
 
      mesh.xExtra = 0
 
      mesh.x = mesh.position.x = random(-this.viewport.width * 0.5this.viewport.width * 0.5)
      mesh.y = mesh.position.y = random(-this.viewport.height * 0.5this.viewport.height * 0.5)
 
      this.meshes.push(mesh)
 
      this.scene.addChild(mesh)
    }
  }
}

然后只需要对它们应用无限滚动逻辑,并遵循与 Media 类中相同的方向进行验证:

update (scroll, direction) {
  this.meshes.forEach(mesh => {
    mesh.position.x = mesh.x - scroll.current * mesh.speed - mesh.xExtra
 
    const viewportOffset = this.viewport.width * 0.5
    const widthTotal = this.viewport.width + mesh.scale.x
 
    mesh.isBefore = mesh.position.x < -viewportOffset
    mesh.isAfter = mesh.position.x > viewportOffset
 
    if (direction === 'right' && mesh.isBefore) {
      mesh.xExtra -= widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    if (direction === 'left' && mesh.isAfter) {
      mesh.xExtra += widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    mesh.position.y += 0.05 * mesh.speed
 
    if (mesh.position.y > this.viewport.height * 0.5 + mesh.scale.y) {
      mesh.position.y -= this.viewport.height + mesh.scale.y
    }
  })
}

就这么简单,现在我们的代码终于完成了。

Reference


[1]

OGL: https://github.com/oframe/ogl

[2]

Three.js: https://threejs.org/

[3]

Babylon.js: https://www.babylonjs.com/



强力推荐前端面试刷题神器
【实战】用 WebGL 创建一个在线画廊


精彩文章回顾,点击直达