- 2015/12/12: Added source map support
Between Grunt, Gulp, Webpack and Broccoli, it seems like a new build tool/asset pipeline/module bundler/etc arrives in the land of JavaScript every few months. While learning WebGL, I started to look into ways to make starting a new project and development a bit easier. I’ve been using Webpack for work-related purposes over the past few months and I’ve really grown to like its easy-to-read config files, async code splitting abilities and wide plugin ecosystem. “Since it helped make my React-based development smooth as butter,” I thought to myself, “perhaps using Webpack would help to make WebGL development go just as smoothly.”
This post follows the steps and rationale used to build a Webpack-based boilerplate for WebGL. If you just want to look at the code, you can grab the source here.
Getting Started
For this build system, all you need to start with is Node.js + npm. We will use Webpack as our module loader/build tool combo. Another invaluable tool in our arsenal will be Babel library for allowing use of ES6+ features. This guide assumes you have basic familiarity with JavaScript and using npm to install modules.
We will start off by installing Webpack globally. I like doing this so I can just use webpack
on
the command line from anywhere:
Webpack is a tool used to bundle files into modules. It has a vast variety of plugins created by both the Webpack team and community to allow modules to be created using both JavaScript and non-JavaScript files. You can even use it in conjunction with tools like Grunt or Gulp! We will be making use of Webpack to transpile our ES6 code to something modern browsers can read with Babel, a plugin that allow us to write our GLSL shader code like you would for a desktop application, and finally bundle all our code into a single (or multiple) JavaScript files.
Let’s create a new directory called webgl_project
:
We will be saving a list of libraries and tools we need to package.json
so let’s create one first:
Using the -y
flag uses default values; we will edit these shortly. Personally, I’d rather edit
these fields in a text editor rather than input them into a prompt.
Next, we will install our dependencies required for development, including:
webpack
: The webpack documentation recommends saving webpack as a local dev. dependencywebpack-dev-server
: Used to watch files & recompile on changes, servers filesbabel-core
: Compiler used to make our ES6 code usable in modern browsersbabel-preset-es2015
: Since version 6, Babel comes with no settings out of the box. This is a preset to compile ES6.babel-loader
: Webpack plugin to transpile JavaScript files before creating modules out of themhtml-webpack-plugin
: A plugin used to auto-generate a HTML file with all webpack modules included.webpack-glsl-loader
: Webpack plugins to load our shaders from files into strings
Let’s install these modules all at once:
At this point, I usually like to open package.json
and start making some edits. This is what I end up with:
We’ll come back to this file to add some commands to use to the scripts
key. For now, let’s hop
back to the terminal.
Project Structure
I wanted the file structure for an intial project to be simple:
This structure can be created with mkdir
& touch
commands.
Now that we have a clear idea of what files are going to be a part of the project, let’s get a Git repository
going and set up a .gitignore
file:
Open up .gitignore
up and enter the following:
Since the contents of these two folders are generated, we shouldn’t commit them to version control.
With that out the way, let us continue by setting up Webpack.
Webpack Configuration
Webpack can be configured a few different ways: with its own configuration files, via the command line with flags,
or as a module in Gulp/Grunt. We’ll be using the config file route. Create a file in the root of your project
directory called webpack.config.js
. Input the following:
Let’s take a look at what’s going on here:
File header
Firstly, we import some modules such as path
and our html-webpack-plugin
. We also define some
constants containing the absolute paths to the folders we wil keep various types of files in, along
with the entry and output paths.
We also check for the presence of the environmental variable NODE_ENV
to determine whether to
build production or development bundles.
entry
The JavaScript entry point of our application.
plugins
The html-webpack-plugin
generates HTML files that already have appropriate script
and link
tags to bundled JS and CSS bundles. We’ll make use of that plugin with a few options. title
is the
value of the title
tag. template
is a path to the HTML file we want to base our index.html off
of. Finally, inject
allows us to control where our script
tags are being created. Valid values
are body
and head
. We will set up our HTML template after configuring Webpack.
output
Here we define where we want Webpack to place the modules it creates.
resolve
resolve
’s root
key lets you tell Webpack which folders to search in when importing one file into another.
For example, imagine the following file structure:
The contents of js/utility/VectorUtils.js
look something like this:
Without setting the resolve.root
property in our Webpack settings, the way to include VectorUtils.js
in Dude.js
would look something like this:
After setting resolve.root
to include our src/js
path, we can treat that as a root directory that Webpack will search through to find other modules. This lets us write import paths like this:
Since we’ve also added src
as a root path, we’ll be able to include shaders just by writing something like this:
This makes importing files easy, no matter what directory you happen to be working in.
module
The loader
key on module
tells Webpack which files to load into modules, and how to load them.
We’ve defined two loaders: one for JavaScript files, and one for our GLSL shaders. test
is a
regular expression that will bundle the files that match it; obviously for a JavaScript loader, we
want Webpack to find modules ending with .js
. We don’t want Webpack to do anything with our
dependencies unless we explicitly import one, so we tell Webpack to exclude
them. The loader
key
is the name of the Webpack plugin used to process JS files. Since we want to be able to write using
ES6 syntax, Babel will handle that responsibility. Because Babel 6 comes with no options out of the
box, we need to tell it to use the es2015
presets package we installed earlier. Enabling directory
caching will give us faster compile times so of course we enable that.
Our last loader is for our shader files, ending with .glsl
. Here, we simply tell Webpack to use
the webpack-glsl
loader.
debug
Some loaders will perform optimizations when building bundles depending on whether the debug
key
is set to true or false.
devtool
At the moment, this tells Webpack which style of source maps to use. source-map
is recommended for
production use only, so we’ll use eval-source-map
which is faster and produces cache-able source
maps.
With that, our configuration of Webpack is complete. Not too bad, eh?
Let’s re-open package.json
and add some commands to scripts
that will make interacting with
webpack a bit easier:
Webpack will check for webpack.config.js
and use it if found. I’m using flags to show build
progress and to give the output a bit more colour. Any key added to the scripts
object can be used
by running npm run <keyname>
. This gives us three easily accessible commands:
NODE_ENV=production npm run build
: Builds our project into/dist/
. Uses a flag to tell Webpack we want loaders to build production-mode modules.npm run watch
: Builds our project and watches files for changes, re-builds on change.npm run dev-server
: Builds our project and watches files for changes, also serves files from a web server. The--inline --hot
flags enable Hot Module replacement, which will update your page without any user input. You can read more about HMR in webpack-dev-server’s documentation.
Now we have one thing left to do, and that is to create our index.html
in /dist
. This is what the contents
look like:
That weird value used in the title
tag allows Webpack to change the page’s title depending on the
value of the title
key passed to html-webpack-plugin
. As you can see, no script
tags are
needed - Webpack will handle that for us.
And with that, the boilerplate is complete. “But Dale,” you exclaim, “I don’t see the point of this. You made a simple Webpack config, so what?”
Motivations
My main motivations behind this came from my own experiences trying to learn WebGL. I’ve noticed that many WebGL tutorials will demonstrate shader usage in WebGL in one of two ways:
- Write a JavaScript string containing the shader code.
- Have a
script
element of typex-ver-shader
and grab the element’s content from JavaScript.
Personally, I don’t like either of these solutions. The first one is just ugly, unreadable and difficult to maintain yet most of the WebGL resources I see introduce shaders using this method. The second is slightly better, but I prefer to keep shaders in their own files, where I can use an editor with syntax highlighting to make readability a bit easier.
I completely understand why tutorial writers do this. WebGL is complex, and wrapping your head around everything needed to render a simple triangle in shader-based GL can be a lot to take in at once. Once you get your head around those concepts, though, then what? What’s the best way to deal with shaders moving forward as a project scales? I’m not claiming this boilerplate is anywhere close to the best, but for me it works. Hopefully it does for you too (and if it doesn’t, please open an issue/PR on the GitHub repo and let me know).
Mini-example: Hello Triangle
Let’s go through a short example of how one would potentially use this boilerplate to easily load
GLSL shaders. Imagine we have two shaders in our src/shaders
directory, called box_vert.glsl
and
box_frag.glsl
containing our vertex and fragment shader, respectively. No need for <script>
tags or multi-line strings! Just import it when you need it, where you need it.
That’s all!
I hope you found this useful in some way. If you have any suggestions on how to improve the boilerplate, please open an issue on the GitHub repo.