How to get started with regl and Webpack
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="https://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 https://localhost:8080/
.
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:
- add
webpack-glsl-loader
todevDependencies
- create
.glsl
files for the vertex shader and for the fragment shader - configure webpack to load
.glsl
files withwebpack-glsl-loader
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
// https://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!