Connect with us

Technology

Creating an Infinite Round Gallery utilizing WebGL with OGL and GLSL Shaders | Codrops


From our sponsor: Elevate all of your advertising and marketing with Mailchimp Smarts.

On this tutorial we’ll implement an infinite round gallery utilizing WebGL with OGL primarily based on the web site Lions Good Information 2020 made by SHIFTBRAIN inc.

A lot of the steps of this tutorial may be additionally reproduced in different WebGL libraries reminiscent of Three.js or Babylon.js with the right variations.

With that being stated, let’s begin coding!

Creating our OGL 3D setting

Step one of any WebGL tutorial is ensuring that you just’re establishing all of the rendering logic required to create a 3D setting.

Often what’s required is: a digital camera, a scene and a renderer that’s going to output every part right into a canvas aspect. Then inside a requestAnimationFrame loop, you’ll use your digital camera to render a scene contained in the renderer. So right here’s our preliminary snippet:

import { Renderer, Digicam, Rework } from 'ogl'
 
export default class App {
  constructor () {
    this.createRenderer()
    this.createCamera()
    this.createScene()
 
    this.onResize()
 
    this.replace()
 
    this.addEventListeners()
  }
 
  createRenderer () {
    this.renderer = new Renderer()
 
    this.gl = this.renderer.gl
    this.gl.clearColor(0.79607843137, 0.79215686274, 0.74117647058, 1)
 
    doc.physique.appendChild(this.gl.canvas)
  }
 
  createCamera () {
    this.digital camera = new Digicam(this.gl)
    this.digital camera.fov = 45
    this.digital camera.place.z = 20
  }
 
  createScene () {
    this.scene = new Rework()
  }
 
  /**
   * Occasions.
   */
  onTouchDown (occasion) {
      
  }
 
  onTouchMove (occasion) {
      
  }
 
  onTouchUp (occasion) {
      
  }
 
  onWheel (occasion) {
      
  }
 
  /**
   * Resize.
   */
  onResize () {
    this.display = {
      top: window.innerHeight,
      width: window.innerWidth
    }
 
    this.renderer.setSize(this.display.width, this.display.top)
 
    this.digital camera.perspective({
      facet: this.gl.canvas.width / this.gl.canvas.top
    })
 
    const fov = this.digital camera.fov * (Math.PI / 180)
    const top = 2 * Math.tan(fov / 2) * this.digital camera.place.z
    const width = top * this.digital camera.facet
 
    this.viewport = {
      top,
      width
    }
  }
 
  /**
   * Replace.
   */
  replace () {
    this.renderer.render({
      scene: this.scene,
      digital camera: this.digital camera
    })
    
    window.requestAnimationFrame(this.replace.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()

Explaining the App class setup

In our createRenderer technique, we’re initializing a renderer with a hard and fast coloration background by calling this.gl.clearColor. Then we’re storing our GL context (this.renderer.gl) reference within the this.gl variable and appending our <canvas> (this.gl.canvas) aspect to our doc.physique.

In our createCamera technique, we’re making a new Digicam() occasion and setting a few of its attributes: fov and its z place. The FOV is the sphere of view of your digital camera, what you’re capable of see from it. And the z is the place of your digital camera within the z axis.

In our createScene technique, we’re utilizing the Rework class, that’s the illustration of a brand new scene that’s going to comprise all our planes that characterize our photos within the WebGL setting.

The onResize technique is crucial a part of our preliminary setup. It’s accountable for three various things:

  1. Ensuring we’re all the time resizing the <canvas> aspect with the right viewport sizes.
  2. Updating our this.digital camera perspective dividing the width and top of the viewport.
  3. Storing within the variable this.viewport, the worth representations that can assist to remodel pixels into 3D setting sizes by utilizing the fov from the digital camera.

The strategy of utilizing the digital camera.fov to remodel pixels in 3D setting sizes is an strategy used fairly often in a number of WebGL implementations. Principally what it does is ensuring that if we do one thing like: this.mesh.scale.x = this.viewport.width; it’s going to make our mesh match the whole display width, behaving like width: 100%, however in 3D house.

And at last in our replace, we’re setting our requestAnimationFrame loop and ensuring we hold rendering our scene.

You’ll additionally discover that we already included the wheel, touchstart, touchmove, touchend, mousedown, mousemove and mouseup occasions, they are going to be used to incorporate person interactions with our software.

Making a reusable geometry occasion

It’s a very good follow to maintain reminiscence utilization low by all the time reusing the identical geometry reference it doesn’t matter what WebGL library you’re utilizing. To characterize all our photos, we’re going to make use of a Airplane geometry, so let’s create a brand new technique and retailer this new geometry contained in the this.planeGeometry variable.

import { Renderer, Digicam, Rework, Airplane } from 'ogl'
 
createGeometry () {
  this.planeGeometry = new Airplane(this.gl, {
    heightSegments: 50,
    widthSegments: 100
  })
}

The rationale for together with heightSegments and widthSegments with these values is having the ability to manipulate vertices in a solution to make the Airplane behave like a paper within the air.

Importing our photos utilizing Webpack

Now it’s time to import our photos into our software. Since we’re utilizing Webpack on this tutorial, all we have to do to request our photos is utilizing import:

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

Now let’s create our array of photos that we wish to use in our infinite slider, so we’re principally going to name the variables above inside a createMedia technique, and use .map to create new cases of the Media class (new Media()), which goes to be our illustration of every picture of the gallery.

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(({ picture, textual content }, index) => {
    const media = new Media({
      geometry: this.planeGeometry,
      gl: this.gl,
      picture,
      index,
      size: this.mediasImages.size,
      scene: this.scene,
      display: this.display,
      textual content,
      viewport: this.viewport
    })
 
    return media
  })
}

As you’ve most likely observed, we’re passing a bunch of arguments to our Media class, I’ll clarify why they’re wanted after we begin establishing the category within the subsequent part. We’re additionally duplicating the quantity of photos to keep away from any problems with not having sufficient photos when making our gallery infinite on very huge screens.

It’s necessary to additionally embrace some particular calls within the onResize and replace strategies for our this.medias array, as a result of we wish the photographs to be responsive:

onResize () {
  if (this.medias) {
    this.medias.forEach(media => media.onResize({
      display: this.display,
      viewport: this.viewport
    }))
  }
}

And likewise do some real-time manipulations contained in the requestAnimationFrame:

replace () {
  this.medias.forEach(media => media.replace(this.scroll, this.course))
}

Establishing the Media class

Our Media class goes to make use of Mesh, Program and Texture courses from OGL to create a 3D aircraft and attribute a texture to it, which in our case goes to be our photos.

In our constructor, we’re going to retailer all variables that we’d like and that had been handed within the new Media() initialization from index.js:

export default class {
  constructor ({ geometry, gl, picture, index, size, renderer, scene, display, textual content, viewport }) {
    this.geometry = geometry
    this.gl = gl
    this.picture = picture
    this.index = index
    this.size = size
    this.scene = scene
    this.display = display
    this.textual content = textual content
    this.viewport = viewport
 
    this.createShader()
    this.createMesh()
 
    this.onResize()
  }
}

Explaining a couple of of those arguments, principally the geometry is the geometry we’re going to use to our Mesh class. The this.gl is our GL context, helpful to maintain doing WebGL manipulations inside the category. The this.picture is the URL of the picture. Each of the this.index and this.size can be used to do positions calculations of the mesh. The this.scene is the group which we’re going to append our mesh to. And at last this.display and this.viewport are the sizes of the viewport and setting.

Now it’s time to create the shader that’s going to be utilized to our Mesh within the createShader technique, in OGL shaders are created with Program:

createShader () {
  const texture = new Texture(this.gl, {
    generateMipmaps: false
  })
 
  this.program = new Program(this.gl, {
    fragment,
    vertex,
    uniforms: {
      tMap: { worth: texture },
      uPlaneSizes: { worth: [0, 0] },
      uImageSizes: { worth: [0, 0] },
      uViewportSizes: { worth: [this.viewport.width, this.viewport.height] }
      },
    clear: true
  })
 
  const picture = new Picture()
 
  picture.src = this.picture
  picture.onload = _ => {
    texture.picture = picture
 
    this.program.uniforms.uImageSizes.worth = [image.naturalWidth, image.naturalHeight]
  }
}

Within the snippet above, we’re principally making a new Texture() occasion, ensuring to make use of generateMipmaps as false so it preserves the standard of the picture. Then making a new Program() occasion, which represents a shader composed of fragment and vertex with some uniforms used to govern it.

We’re additionally making a new Picture() occasion to preload the picture earlier than making use of it to the texture.picture. And likewise updating the this.program.uniforms.uImageSizes.worth as a result of it’s going for use to protect the facet ratio of our photos.

It’s necessary to create our fragment and vertex shaders now, so we’re going to create two new information: fragment.glsl and vertex.glsl:

precision highp float;
 
uniform vec2 uImageSizes;
uniform vec2 uPlaneSizes;
uniform sampler2D tMap;
 
various vec2 vUv;
 
void principal() {
  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 place;
attribute vec2 uv;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
various vec2 vUv;
 
void principal() {
  vUv = uv;
 
  vec3 p = place;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

And require them within the begin of Media.js utilizing Webpack:

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

Now let’s create our new Mesh() occasion within the createMesh technique merging collectively the geometry and shader.

createMesh () {
  this.aircraft = new Mesh(this.gl, {
    geometry: this.geometry,
    program: this.program
  })
 
  this.aircraft.setParent(this.scene)
}

The Mesh occasion is saved within the this.aircraft variable to be reused within the onResize and replace strategies, then appended as a toddler of the this.scene group.

The one factor now we have now on the display is an easy sq. with our picture:

Let’s now implement the onResize technique and ensure we’re rendering rectangles:

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

The scale.y and scale.x calls are accountable for scaling our aspect correctly, remodeling our earlier sq. right into a rectangle of 700×900 sizes primarily based on the size.

And the uViewportSizes and uPlaneSizes uniform worth updates makes the picture show accurately. That’s principally what makes the picture have the background-size: cowl; conduct, however in WebGL setting.

Now we have to place all of the rectangles within the x axis, ensuring now we have a small hole between them. To realize that, we’re going to make use of this.aircraft.scale.x, this.padding and this.index variables to do the calculation required to maneuver them:

this.padding = 2
 
this.width = this.aircraft.scale.x + this.padding
this.widthTotal = this.width * this.size
 
this.x = this.width * this.index

And within the replace technique, we’re going to set the this.aircraft.place to those variables:

replace () {
  this.aircraft.place.x = this.x
}

Now you’ve setup all of the preliminary code of Media, which ends up in the next picture:

Together with infinite scrolling logic

Now it’s time to make it attention-grabbing and embrace scrolling logic on it, so now we have a minimum of an infinite gallery in place when the person scrolls by way of your web page. In our index.js, we’ll do the next updates.

First, let’s embrace a brand new object known as this.scroll in our constructor with all variables that we are going to manipulate to do the graceful scrolling:

this.scroll = {
  ease: 0.05,
  present: 0,
  goal: 0,
  final: 0
}

Now let’s add the contact and wheel occasions, so when the person interacts with the canvas, he’ll have the ability to transfer stuff:

onTouchDown (occasion) {
  this.isDown = true
 
  this.scroll.place = this.scroll.present
  this.begin = occasion.touches ? occasion.touches[0].clientX : occasion.clientX
}
 
onTouchMove (occasion) {
  if (!this.isDown) return
 
  const x = occasion.touches ? occasion.touches[0].clientX : occasion.clientX
  const distance = (this.begin - x) * 0.01
 
  this.scroll.goal = this.scroll.place + distance
}
 
onTouchUp (occasion) {
  this.isDown = false
}

Then, we’ll embrace the NormalizeWheel library in onWheel occasion, so this fashion now we have the identical worth on all browsers when the person scrolls:

onWheel (occasion) {
  const normalized = NormalizeWheel(occasion)
  const velocity = normalized.pixelY
 
  this.scroll.goal += velocity * 0.005
}

In our replace technique with requestAnimationFrame, we’ll lerp the this.scroll.present with this.scroll.goal to make it easy, then we’ll go it to all medias:

replace () {
  this.scroll.present = lerp(this.scroll.present, this.scroll.goal, this.scroll.ease)
 
  if (this.medias) {
    this.medias.forEach(media => media.replace(this.scroll))
  }
 
  this.scroll.final = this.scroll.present
 
  window.requestAnimationFrame(this.replace.bind(this))
}

And now we simply replace our Media file to make use of the present scroll worth to maneuver the Mesh to the brand new scroll place:

replace (scroll) {
  this.aircraft.place.x = this.x - scroll.present * 0.1
}

That is the present consequence now we have:

As you’ve observed, it’s not infinite but, to realize that, we have to embrace some further code. Step one is together with the course of the scroll within the replace technique from index.js:

replace () {
  this.scroll.present = lerp(this.scroll.present, this.scroll.goal, this.scroll.ease)
 
  if (this.scroll.present > this.scroll.final) {
    this.course = 'proper'
  } else {
    this.course = 'left'
  }
 
  if (this.medias) {
    this.medias.forEach(media => media.replace(this.scroll, this.course))
  }
 
  this.scroll.final = this.scroll.present
}

Now within the Media class, you have to embrace a variable known as this.further within the constructor, and do some manipulations on it to sum the whole width of the gallery, when the aspect is exterior of the display.

constructor ({ geometry, gl, picture, index, size, renderer, scene, display, textual content, viewport }) {
  this.further = 0
}

replace (scroll) {
  this.aircraft.place.x = this.x - scroll.present * 0.1 - this.further
    
  const planeOffset = this.aircraft.scale.x / 2
  const viewportOffset = this.viewport.width
 
  this.isBefore = this.aircraft.place.x + planeOffset < -viewportOffset
  this.isAfter = this.aircraft.place.x - planeOffset > viewportOffset
 
  if (course === 'proper' && this.isBefore) {
    this.further -= this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
 
  if (course === 'left' && this.isAfter) {
    this.further += this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
}

That’s it, now now we have the infinite scrolling gallery, fairly cool proper?

Together with round rotation

Now it’s time to incorporate the particular taste of the tutorial, which is making the infinite scrolling even have the round rotation. To realize it, we’ll use Math.cos to vary the this.mesh.place.y accordingly to the rotation of the aspect. And map method to vary the this.mesh.rotation.z primarily based on the aspect place within the z axis.

First, let’s make it rotate in a easy manner primarily based on the place. The map technique is principally a solution to serve values primarily based on one other particular vary, let’s say for instance you utilize map(0.5, 0, 1, -500, 500);, it’s going to return 0 as a result of it’s the center between -500 and 500. Principally the primary argument controls the output of min2 and max2:

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

Let’s see it in motion by together with the next like of code within the Media class:

this.aircraft.rotation.z = map(this.aircraft.place.x, -this.widthTotal, this.widthTotal, Math.PI, -Math.PI)

And that’s the consequence we get up to now. It’s already fairly cool since you’re capable of see the rotation altering primarily based on the aircraft place:

Now it’s time to make it look round. Let’s use Math.cos, we simply have to do a easy calculation with this.aircraft.place.x / this.widthTotal, this fashion we’ll have a cos that can return a normalized worth that we will simply tweak multiplying by how a lot we wish to change the y place of the aspect:

this.aircraft.place.y = Math.cos((this.aircraft.place.x / this.widthTotal) * Math.PI) * 75 - 75

Easy as that, we’re simply shifting it by 75 in setting house primarily based within the place, this offers us the next consequence, which is precisely what we needed to realize:

Snapping to the closest merchandise

Now let’s embrace a easy snapping to the closest merchandise when the person stops scrolling. To realize that, we have to create a brand new technique known as onCheck, it’s going to do some calculations when the person releases the scrolling:

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

The results of the merchandise variable is all the time the middle of one of many parts within the gallery, which snaps the person to the corresponding place.

For wheel occasions, we’d like a debounced model of it known as onCheckDebounce that we will embrace within the constructor by together with lodash/debounce:

import debounce from 'lodash/debounce'
 
constructor ({ digital camera, coloration, gl, renderer, scene, display, url, viewport }) {
  this.onCheckDebounce = debounce(this.onCheck, 200)
}
 
onWheel (occasion) {
  this.onCheckDebounce()
}

Now the gallery is all the time being snapped to the right entry:

Writing paper shaders

Lastly let’s embrace essentially the most attention-grabbing a part of our undertaking, which is enhancing the shaders somewhat bit by making an allowance for the scroll velocity and distorting the vertices of our meshes.

Step one is to incorporate two new uniforms in our this.program declaration from Media class: uSpeed and uTime.

this.program = new Program(this.gl, {
  fragment,
  vertex,
  uniforms: {
    tMap: { worth: texture },
    uPlaneSizes: { worth: [0, 0] },
    uImageSizes: { worth: [0, 0] },
    uViewportSizes: { worth: [this.viewport.width, this.viewport.height] },
    uSpeed: { worth: 0 },
    uTime: { worth: 0 }
  },
  clear: true
})

Now let’s write some shader code to make our photos bend and warp in a really cool manner. In your vertex.glsl file, it’s best to embrace the brand new uniforms: uniform float uTime and uniform float uSpeed:

uniform float uTime;
uniform float uSpeed;

Then contained in the void principal() of your shader, now you can manipulate the vertices within the z axis utilizing these two values plus the place saved variable in p. We’re going to make use of a sin and cos to bend our vertices prefer it’s a aircraft, so all you have to do is together with the next line:

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

Additionally don’t overlook to incorporate uTime increment within the replace() technique from Media:

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

Simply this line of code outputs a reasonably cool paper impact animation:

Together with textual content in WebGL utilizing MSDF fonts

Now let’s embrace our textual content contained in the WebGL, to realize that, we’re going to make use of msdf-bmfont to generate our information, you may see how to try this on this GitHub repository, however principally it’s putting in the npm dependency and operating the command beneath:

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

After operating it, it’s best to now have a .png and .json file in the identical listing, these are the information that we’re going to make use of on our MSDF implementation in OGL.

Now let’s create a brand new file known as Title and begin establishing the code of it. First let’s create our class and use import within the shaders and the information:

import AutoBind from 'auto-bind'
import { Shade, Geometry, Mesh, Program, Textual content, 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, aircraft, renderer, textual content }) {
    AutoBind(this)
 
    this.gl = gl
    this.aircraft = aircraft
    this.renderer = renderer
    this.textual content = textual content
 
    this.createShader()
    this.createMesh()
  }
}

Now it’s time to begin establishing MSDF implementation code contained in the createShader() technique. The very first thing we’re going to do is create a new Texture() occasion and cargo the fonts/freight.png one saved in src:

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

Then we have to begin establishing the fragment shader we’re going to make use of to render the MSDF textual content, as a result of MSDF may be optimized in WebGL 2.0, we’re going to make use of this.renderer.isWebgl2 from OGL to examine if it’s supported or not and declare completely different shaders primarily based on it, so we’ll have vertex300, fragment300, vertex100 and fragment100:

createShader () {
  const vertex100 = `${vertex}`
 
  const fragment100 = `
    #extension GL_OES_standard_derivatives : allow
 
    precision highp float;
 
    ${fragment}
  `
 
  const vertex300 = `#model 300 es
 
    #outline attribute in
    #outline various out
 
    ${vertex}
  `
 
  const fragment300 = `#model 300 es
 
    precision highp float;
 
    #outline various in
    #outline texture2D texture
    #outline 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,
    clear: true,
    fragment: fragmentShader,
    vertex: vertexShader,
    uniforms: {
      uColor: { worth: new Shade('#545050') },
      tMap: { worth: texture }
    }
  })
}

As you’ve most likely observed, we’re prepending fragment and vertex with completely different setup primarily based on the renderer WebGL model, let’s create additionally our text-fragment.glsl and text-vertex.glsl information:

uniform vec3 uColor;
uniform sampler2D tMap;
 
various vec2 vUv;
 
void principal() {
  vec3 coloration = texture2D(tMap, vUv).rgb;
 
  float signed = max(min(coloration.r, coloration.g), min(max(coloration.r, coloration.g), coloration.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 place;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
various vec2 vUv;
 
void principal() {
  vUv = uv;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
}

Lastly let’s create the geometry of our MSDF font implementation within the createMesh() technique, for that we’ll use the new Textual content() occasion from OGL, after which apply the buffers generated from it to the new Geometry() occasion:

createMesh () {
  const textual content = new Textual content({
    align: 'heart',
    font,
    letterSpacing: -0.05,
    dimension: 0.08,
    textual content: this.textual content,
    wordSpacing: 0,
  })
 
  const geometry = new Geometry(this.gl, {
    place: { dimension: 3, information: textual content.buffers.place },
    uv: { dimension: 2, information: textual content.buffers.uv },
    id: { dimension: 1, information: textual content.buffers.id },
    index: { information: textual content.buffers.index }
  })
 
  geometry.computeBoundingBox()
 
  this.mesh = new Mesh(this.gl, { geometry, program: this.program })
  this.mesh.place.y = -this.aircraft.scale.y * 0.5 - 0.085
  this.mesh.setParent(this.aircraft)
}

Now let’s apply our model new titles within the Media class, we’re going to create a brand new technique known as createTilte() and apply it to the constructor:

constructor ({ geometry, gl, picture, index, size, renderer, scene, display, textual content, viewport }) {
  this.createTitle()
}

createTitle () {
  this.title = new Title({
    gl: this.gl,
    aircraft: this.aircraft,
    renderer: this.renderer,
    textual content: this.textual content,
  })
}

Easy as that, we’re simply together with a new Title() occasion inside our Media class, this can output the next consequence for you:

Among the finest issues about rendering textual content inside WebGL is decreasing the overload of calculations required by the browser when animating the textual content to the appropriate place. When you go together with the DOM strategy, you’ll normally have somewhat little bit of efficiency influence as a result of browsers might want to recalculate DOM sections when translating the textual content correctly and checking composite layers.

For the aim of this demo, we additionally included a new Quantity() class implementation that can be accountable for exhibiting the present index that the person is seeing. You’ll be able to examine the way it’s carried out in supply code, nevertheless it’s principally the identical implementation of the Title class with the one distinction of it loading a distinct font model:

Together with background blocks

To finalize the demo, let’s implement some blocks within the background that can be shifting in x and y axis to boost the depth impact of it:

To realize this impact we’re going to create a brand new Background class and inside it we’ll initialize some new Airplane() geometries in a new Mesh() with random sizes and positions by altering the scale and place of the meshes of the for loop:

import { Shade, Mesh, Airplane, 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 Airplane(this.gl)
    const program = new Program(this.gl, {
      vertex,
      fragment,
      uniforms: {
        uColor: { worth: new Shade('#c4c3b6') }
      },
      clear: 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.velocity = random(0.75, 1)
 
      mesh.xExtra = 0
 
      mesh.x = mesh.place.x = random(-this.viewport.width * 0.5, this.viewport.width * 0.5)
      mesh.y = mesh.place.y = random(-this.viewport.top * 0.5, this.viewport.top * 0.5)
 
      this.meshes.push(mesh)
 
      this.scene.addChild(mesh)
    }
  }
}

Then after that we simply want to use infinite scrolling logic on them as properly, following the identical directional validation now we have within the Media class:

replace (scroll, course) {
  this.meshes.forEach(mesh => {
    mesh.place.x = mesh.x - scroll.present * mesh.velocity - mesh.xExtra
 
    const viewportOffset = this.viewport.width * 0.5
    const widthTotal = this.viewport.width + mesh.scale.x
 
    mesh.isBefore = mesh.place.x < -viewportOffset
    mesh.isAfter = mesh.place.x > viewportOffset
 
    if (course === 'proper' && mesh.isBefore) {
      mesh.xExtra -= widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    if (course === 'left' && mesh.isAfter) {
      mesh.xExtra += widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    mesh.place.y += 0.05 * mesh.velocity
 
    if (mesh.place.y > this.viewport.top * 0.5 + mesh.scale.y) {
      mesh.place.y -= this.viewport.top + mesh.scale.y
    }
  })
}

That’s easy as that, now now we have the blocks within the background as properly, finalizing the code of our demo!

I hope this tutorial was helpful to you and don’t overlook to remark if in case you have any questions!

Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *