前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【实战】用 WebGL 创建一个在线画廊

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

作者头像
疯狂的技术宅
发布2021-03-16 11:17:34
2.9K0
发布2021-03-16 11:17:34
举报
文章被收录于专栏:京程一灯京程一灯
代码语言:javascript
复制
// 每日前端夜话 第498篇
// 正文共:6000 字
// 预计阅读时间:15 分钟

目录

  • 创建 OGL 3D 环境
  • 解释 `App` 类的设置
  • 创建可重用的几何实例
  • 用 Webpack 导入图像
  • 设置 `Media` 类
  • 添加无限滚动逻辑
  • 加入圆周旋转
  • 捕捉到最接近的项目
  • 编写着色器
  • 用MSDF字体在WebGL中包含文本
  • 引入背景块

本文源码在公众号对话框中回复: 0311 领取。

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

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

创建 OGL 3D 环境

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

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

代码语言:javascript
复制
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.79607843137, 0.79215686274, 0.74117647058, 1)
 
    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 = {
      height: window.innerHeight,
      width: window.innerWidth
    }
 
    this.renderer.setSize(this.screen.width, this.screen.height)
 
    this.camera.perspective({
      aspect: this.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({
      scene: this.scene,
      camera: this.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 变量中。

代码语言:javascript
复制
import { Renderer, Camera, Transform, Plane } from 'ogl'
 
createGeometry () {
  this.planeGeometry = new Plane(this.gl, {
    heightSegments: 50,
    widthSegments: 100
  })
}

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

用 Webpack 导入图像

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

代码语言:javascript
复制
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()),它将用来表示画廊程序中每个图片。

代码语言:javascript
复制
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({
      geometry: this.planeGeometry,
      gl: this.gl,
      image,
      index,
      length: this.mediasImages.length,
      scene: this.scene,
      screen: this.screen,
      text,
      viewport: this.viewport
    })
 
    return media
  })
}

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

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

代码语言:javascript
复制
onResize () {
  if (this.medias) {
    this.medias.forEach(media => media.onResize({
      screen: this.screen,
      viewport: this.viewport
    }))
  }
}

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

代码语言:javascript
复制
update () {
  this.medias.forEach(media => media.update(this.scroll, this.direction))
}

设置 Media

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

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

代码语言:javascript
复制
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 创建的:

代码语言:javascript
复制
createShader () {
  const texture = new Texture(this.gl, {
    generateMipmaps: false
  })
 
  this.program = new Program(this.gl, {
    fragment,
    vertex,
    uniforms: {
      tMap: { value: texture },
      uPlaneSizes: { value: [0, 0] },
      uImageSizes: { value: [0, 0] },
      uViewportSizes: { value: [this.viewport.width, this.viewport.height] }
      },
    transparent: true
  })
 
  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

代码语言:javascript
复制
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 开头中导入它们:

代码语言:javascript
复制
import fragment from './fragment.glsl'
import vertex from './vertex.glsl'

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

代码语言:javascript
复制
createMesh () {
  this.plane = new Mesh(this.gl, {
    geometry: this.geometry,
    program: this.program
  })
 
  this.plane.setParent(this.scene)
}

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

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

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

代码语言:javascript
复制
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; 行为。

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

代码语言:javascript
复制
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 设置为以下变量:

代码语言:javascript
复制
update () {
  this.plane.position.x = this.x
}

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

添加无限滚动逻辑

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

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

代码语言:javascript
复制
this.scroll = {
  ease: 0.05,
  current: 0,
  target: 0,
  last: 0
}

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

代码语言:javascript
复制
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 库,这样当用户滚动时,在所有浏览器上能得到有相同的值:

代码语言:javascript
复制
onWheel (event) {
  const normalized = NormalizeWheel(event)
  const speed = normalized.pixelY
 
  this.scroll.target += speed * 0.005
}

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

代码语言:javascript
复制
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 移到新的滚动位置:

代码语言:javascript
复制
update (scroll) {
  this.plane.position.x = this.x - scroll.current * 0.1
}

下面是目前的成果:

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

代码语言:javascript
复制
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 的变量,并对它进行一些操作,当元素位于屏幕外部时求出图库的总宽度。

代码语言:javascript
复制
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
  }
}

现在可以无限滚动了。

加入圆周旋转

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

代码语言:javascript
复制
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 类中添加以下类似的代码来观察它的作用:

代码语言:javascript
复制
this.plane.rotation.z = map(this.plane.position.x, -this.widthTotal, this.widthTotal, Math.PI, -Math.PI)

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

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

代码语言:javascript
复制
this.plane.position.y = Math.cos((this.plane.position.x / this.widthTotal) * Math.PI) * 75 - 75

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

捕捉到最接近的项目

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

代码语言:javascript
复制
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 将其添加到构造函数中:

代码语言:javascript
复制
import debounce from 'lodash/debounce'
 
constructor ({ camera, color, gl, renderer, scene, screen, url, viewport }) {
  this.onCheckDebounce = debounce(this.onCheck, 200)
}
 
onWheel (event) {
  this.onCheckDebounce()
}

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

编写着色器

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

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

代码语言:javascript
复制
this.program = new Program(this.gl, {
  fragment,
  vertex,
  uniforms: {
    tMap: { value: texture },
    uPlaneSizes: { value: [0, 0] },
    uImageSizes: { value: [0, 0] },
    uViewportSizes: { value: [this.viewport.width, this.viewport.height] },
    uSpeed: { value: 0 },
    uTime: { value: 0 }
  },
  transparent: true
})

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

代码语言:javascript
复制
uniform float uTime;
uniform float uSpeed;

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

代码语言:javascript
复制
p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5);

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

代码语言:javascript
复制
this.program.uniforms.uTime.value += 0.04

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

用MSDF字体在WebGL中包含文本

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

代码语言:javascript
复制
msdf-bmfont -f json -m 1024,1024 -d 4 --pot --smart-size freight.otf

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

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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, {
    cullFace: null,
    depthTest: false,
    depthWrite: false,
    transparent: true,
    fragment: fragmentShader,
    vertex: vertexShader,
    uniforms: {
      uColor: { value: new Color('#545050') },
      tMap: { value: texture }
    }
  })
}

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

代码语言:javascript
复制
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() 实例:

代码语言:javascript
复制
createMesh () {
  const text = new Text({
    align: 'center',
    font,
    letterSpacing: -0.05,
    size: 0.08,
    text: this.text,
    wordSpacing: 0,
  })
 
  const geometry = new Geometry(this.gl, {
    position: { size: 3, data: text.buffers.position },
    uv: { size: 2, data: text.buffers.uv },
    id: { size: 1, data: text.buffers.id },
    index: { data: text.buffers.index }
  })
 
  geometry.computeBoundingBox()
 
  this.mesh = new Mesh(this.gl, { geometry, program: this.program })
  this.mesh.position.y = -this.plane.scale.y * 0.5 - 0.085
  this.mesh.setParent(this.plane)
}

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

代码语言:javascript
复制
constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
  this.createTitle()
}

createTitle () {
  this.title = new Title({
    gl: this.gl,
    plane: this.plane,
    renderer: this.renderer,
    text: this.text,
  })
}

将输出以下结果:

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

引入背景块

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

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

代码语言:javascript
复制
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: { value: new Color('#c4c3b6') }
      },
      transparent: true
    })
 
    this.meshes = []
 
    for (let i = 0; i < 50; i++) {
      let mesh = new Mesh(this.gl, {
        geometry,
        program,
      })
 
      const scale = random(0.75, 1)
 
      mesh.scale.x = 1.6 * scale
      mesh.scale.y = 0.9 * scale
 
      mesh.speed = random(0.75, 1)
 
      mesh.xExtra = 0
 
      mesh.x = mesh.position.x = random(-this.viewport.width * 0.5, this.viewport.width * 0.5)
      mesh.y = mesh.position.y = random(-this.viewport.height * 0.5, this.viewport.height * 0.5)
 
      this.meshes.push(mesh)
 
      this.scene.addChild(mesh)
    }
  }
}

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

代码语言:javascript
复制
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/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端先锋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 创建 OGL 3D 环境
  • 解释 App 类的设置
  • 创建可重用的几何实例
  • 用 Webpack 导入图像
  • 设置 Media 类
  • 添加无限滚动逻辑
  • 加入圆周旋转
  • 捕捉到最接近的项目
  • 编写着色器
  • 用MSDF字体在WebGL中包含文本
  • 引入背景块
    • Reference
    相关产品与服务
    图像处理
    图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档