• Giacomo Debidda
    • Blog
    • Projects
    • About
    • Contact

How to get started with regl and Webpack

31 Oct 2017
  • regl
  • webgl
  • webpack

Table of Contents πŸ“‘
  1. # regl and WebGL
  2. # Project structure and boilerplate
  3. # Your first regl command
  4. # One canvas, two sizes
  5. # Batch rendering
  6. # GLSL? There is a loader for that!
  7. # Hungarian notation?
  8. # Conclusion

I have been wanting to play around with the regl library since I wathed the talk that Mikola Lysenko gave at PLOTCON 2016, and this week I finally decided to invest some time in setting up a repo with Webpack and some regl examples.

regl is a pretty new library, but it seems quite popular among data visualization practitioners. Jim Vallandingham and Peter Beshai wrote really nice tutorials about regl, and Nadieh Bremer created the stunning visualization β€œA breathing Earth”.

# regl and WebGL

Before starting to learn about regl, you need some basic knowledge about WebGL, the low level API to draw 3D graphics in the browser, and its graphics pipeline (aka rendering pipeline). This article on WebGL Fundamentals does a great job in explaining what WebGL is:

WebGL is just a rasterization engine. It draws points, lines, and triangles based on code you supply.

WebGL runs on the GPU on your computer, and you need to provide the code in the form of two functions: a vertex shader and a fragment shader.

Another important thing to know is that WebGL is a state machine: once you modify an attributes, that modification is permanent until you modify that attribute again.

regl is functional abstration of WebGL. It simplifies WebGL programming by removing as much shared state as it can get away with.

In regl there are two fundamentals abstractions: resources and commands.

  • A resource is a handle to something that you load on the GPU, like a texture.
  • A command is a complete representation of the WebGL state required to perform some draw call. It wraps it up and packages it into a single reusable function.

In this article I will only talk about commands.

# Project structure and boilerplate

I found out that if I want to learn a new technology/library/tool I have to play around with it, so I created a repo and I called it regl-playground.

Let’s start defining the structure for this repo. Here is the root directory:

.
β”œβ”€β”€ package.json
β”œβ”€β”€ README.md
β”œβ”€β”€ src
└── webpack.config.js

And here is the src directory.

.
β”œβ”€β”€ css
β”‚ └── main.css
β”œβ”€β”€ js
β”‚ β”œβ”€β”€ index.js
β”‚ └── one-shot-rendering.js
└── templates
β”œβ”€β”€ index.html
└── one-shot-rendering.html

You will need regl and a few dev dependencies for webpack. If you want to save some time (and keystrokes), you can copy the package.json down below and install all you need with yarn install.

{
"name": "regl-playground",
"version": "1.0.0",
"main": "index.js",
"repository": "git@github.com:jackdbd/regl-playground.git",
"author": "jackdbd <jackdebidda@gmail.com>",
"license": "MIT",
"scripts": {
"dev": "webpack-dev-server --config webpack.config.js",
"lint": "eslint src",
"build": "webpack --config webpack.config.js --progress"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.24.1",
"clean-webpack-plugin": "^0.1.17",
"eslint": "^4.5.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.7.0",
"extract-text-webpack-plugin": "^3.0.1",
"html-webpack-plugin": "^2.30.1",
"style-loader": "^0.19.0",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.3"
},
"dependencies": {
"regl": "^1.3.0"
}
}

Next, you will need 2 HTML files, one for the index, one for the actual regl application. I think it’s a good idea to put these files in src/templates.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Home</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Home of regl-playground">
<!-- bundle.css is injected here by html-webpack-plugin -->
</head>
<body>
<header>
<h1>List of regl examples</h1>
</header>
<ul>
<li><a href="/one-shot-rendering.html">one-shot rendering</a></li>
</ul>
<p>For documentation, see the <a href="http://regl.party/" target="_blank">regl API</a>.</p>
<footer>Examples with regl version <code>1.3.0</code></footer>
<!-- bundle.js is injected here by html-webpack-plugin -->
</body>
</html>
<!-- one-shot-rendering.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>One shot rendering</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="regl example with one shot rendering">
<!-- bundle.css is injected here by html-webpack-plugin -->
</head>
<body>
<ul>
<li><a href="/index.html">Home</a></li>
</ul>
<h1>regl one shot rendering example</h1>
<!-- bundle.js is injected here by html-webpack-plugin -->
</body>
</html>

Then, create a minimal CSS file in src/css.

/* main.css */
h1 {
color: #0b4192;
}

And the Javascript files. We’ll put the code in one-shot-rendering.js later on. For now, just import the CSS, so you can check that webpack is setup correctly.

// index.js
import '../css/main.css'
// one-shot-rendering.js
import '../css/main.css'

Finally, the webpack configuration. I like to include BundleAnalyzerPlugin to check the bundle sizes.

// webpack.config.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
home: path.join(__dirname, 'src', 'js', 'index.js'),
'one-shot-rendering': path.join(
__dirname,
'src',
'js',
'one-shot-rendering.js'
),
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[chunkhash].bundle.js',
sourceMapFilename: '[file].map',
},
module: {
rules: [
// rule for .js/.jsx files
{
test: /\.(js|jsx)$/,
include: [path.join(__dirname, 'js', 'src')],
exclude: [path.join(__dirname, 'node_modules')],
use: {
loader: 'babel-loader',
},
},
// rule for css files
{
test: /\.css$/,
include: path.join(__dirname, 'src', 'css'),
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader',
}),
},
],
},
target: 'web',
devtool: 'source-map',
plugins: [
new BundleAnalyzerPlugin(),
new CleanWebpackPlugin(['dist'], {
root: __dirname,
exclude: ['favicon.ico'],
verbose: true,
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src', 'templates', 'index.html'),
hash: true,
filename: 'index.html',
chunks: ['commons', 'home'],
}),
new HtmlWebpackPlugin({
template: path.join(
__dirname,
'src',
'templates',
'one-shot-rendering.html'
),
hash: true,
filename: 'one-shot-rendering.html',
chunks: ['commons', 'one-shot-rendering'],
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: '[name].[chunkhash].bundle.js',
chunks: ['home', 'one-shot-rendering'],
}),
new ExtractTextPlugin('[name].[chunkhash].bundle.css'),
],
devServer: {
host: 'localhost',
port: 8080,
contentBase: path.join(__dirname, 'dist'),
inline: true,
stats: {
colors: true,
reasons: true,
chunks: false,
modules: false,
},
},
performance: {
hints: 'warning',
},
}

Ah, don’t forget .babelrc!

{
  "presets": ["es2015"]
}

Check that everything works by running yarn run dev and going to http://localhost:8080/.

Five triangle with batch rendering and the frame loop

# GLSL? There is a loader for that!

I don’t know about you, but defining shaders with back ticks looks awful to me. It would be much better to write .glsl files and then load them in a regl application. We could use linters and autocompletion, but even more importantly, we would avoid copy pasting the shaders in JS every single time we need them.

Luckily with Webpack it’s pretty easy to fix this issue. There is a loader for that!

Ok, so you need to do these things:

  1. add webpack-glsl-loader to devDependencies
  2. create .glsl files for the vertex shader and for the fragment shader
  3. configure webpack to load .glsl files with webpack-glsl-loader
  4. require the shaders in the regl application

As an example, we’ll remove the shader code between the back ticks in batch-rendering.js and we’ll use the GLSL loader instead.

Install the GLSL loader with yarn add --dev webpack-glsl-loader.

Create GLSL files for your shaders. Create src/glsl/vertex/batch.glsl for the vertex shader and src/glsl/fragment/batch.glsl for the fragment shader. You just have to copy and paste the code between the back ticks in batch-rendering.js.

Configure webpack to use webpack-glsl-loader to load .glsl files.

// webpack.config.js
// rule for .glsl files (shaders)
{
test: /\.glsl$/,
use: [
{
loader: 'webpack-glsl-loader',
},
],
},

Finally, require your shaders in batch-rendering.js.

// batch-rendering.js
import '../css/main.css'

const vertexShader = require('../glsl/vertex/batch.glsl')
const fragmentShader = require('../glsl/fragment/batch.glsl')

const canvas = document.getElementById('regl-canvas')
const regl = require('regl')({
canvas,
})
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight

// regl render command to draw a SINGLE triangle
const drawTriangle = regl({
vert: vertexShader,
frag: fragmentShader,

viewport: {
x: 0,
y: 0,
width: canvas.width,
height: canvas.height,
},

attributes: {
// [x,y] positions of the 3 vertices (without the offset)
position: [-0.25, 0.0, 0.5, 0.0, -0.1, -0.5],
},

uniforms: {
// Destructure context and pass only tick. Pass props just because we need
// to pass a third argument: batchId, which gives the index of the regl
// 'drawTriangle' render command.
color: ({ tick }, props, batchId) => {
const r = Math.sin(
0.02 * ((0.1 + Math.sin(batchId)) * (tick + 3.0 * batchId))
)
const g = Math.cos(0.02 * (0.02 * tick + 0.1 * batchId))
const b = Math.sin(
0.02 * ((0.3 + Math.cos(2.0 * batchId)) * tick + 0.8 * batchId)
)
const alpha = 1.0
return [r, g, b, alpha]
},
angle: ({ tick }) => 0.01 * tick,
offset: regl.prop('offset'),
},

// disable the depth buffer
// http://learningwebgl.com/blog/?p=859
depth: {
enable: false,
},

count: 3,
})

// Here we register a per-frame callback to draw the whole scene
regl.frame(() => {
regl.clear({
color: [0.0, 0.0, 0.0, 1.0], // r, g, b, a
})

/* In batch rendering a regl rendering command can be executed multiple times
by passing a non-negative integer or an array as the first argument.
The batchId is initially 0 and incremented each time the render command is
executed.
Note: this command draws a SINGLE triangle, but since we are passing an
array of 5 elements it is executed 5 times. */

drawTriangle([
{ offset: [0.0, 0.0] }, // props0
{ offset: [-0.15, -0.15] }, // props1...
{ offset: [0.15, 0.15] },
{ offset: [-0.5, 0.5] },
{ offset: [0.5, -0.5] },
])
})

# Hungarian notation?

On WebGL Fundamentals they say that it’s common practice to place a letter in front of the variables to indicate their type: u for uniforms, a for attributes and the v for varyings. I’m not a huge fan of this Hungarian notation though. I don’t think it really improves the readability of the code and I don’t think I will use it.

# Conclusion

I plan to write several articles about regl in the near future. In this one we learned how to configure Webpack for regl applications. In the next one I will create a few examples with d3 and regl.

Stay tuned!


  • regl
  • webgl
  • webpack
  • RSS
  • GitHub
  • Twitter
  • Linkedin
  • Stack Overflow
Copyright Β© 2020 – 2022 Giacomo Debidda – All rights reserved