phixl
A library for WebGL which is for people who want to write their own shaders.
Overview
This library provides a layer of abstraction over WebGL which allows users to apply a shader to a canvas without having to worry about the WebGL API at all.
The library exports a factory function called Shader
which takes the GLSL code
as strings and the data you want to supply to the shader as arguments.
Shader
will return a function which will render that shader to a canvas.
You can also use the function returned by Shader
to render to a texture using
WebGLFramebuffer
. This lefts you sample from the result of other shaders as
textures. This lets your shaders become composable and lets you easily write
programs which take advantage of the parallelization of the GPU.
Quickstart
Say you wanted to render a cube using phixl and WebGL using the following
data for gl.drawElements
. For simplicity, we'll just color the cube using
the normal vectors.
const CUBE_INDICES = new Uint16Array([...]);
const CUBE_VERTICES = new Float32Array([...]);
const CUBE_NORMALS = new Float32Array([...]);
Let's set up your vertex shader:
precision mediump float;
attribute vec3 a_Vertex;
attribute vec3 a_Normal;
varying vec3 v_Normal;
uniform mat4 u_ModelMat;
uniform mat4 u_ViewMat;
uniform mat4 u_PerspectiveMat;
void main() {
gl_Position =
u_PerspectiveMat * u_ViewMat * u_ModelMat * vec4(a_Vertex, 1.0);
v_Normal = a_Normal;
}
The fragment shader is straightforward:
precision mediump float;
varying vec3 v_Normal;
void main() {
gl_FragColor = vec4(abs(v_Normal), 1.0);
}
Let's say you are using glslifyloader
and rawloader
with Webpack to load the
shaders into JS strings:
const vertexSrc = require('vertex.glsl').default;
const fragmentSrc = require('fragment.glsl').default;
Now we want to import the relevant functions from phixl and create a shader:
const {
ModelMatUniform,
NormalMatUniform,
PerspectiveMatUniform,
Shader,
Vec3Attribute,
ViewMatUniform,
} = require('phixl');
const aVertex = Vec3Attribute('a_Vertex', CUBE_VERTICES);
const aNormal = Vec3Attribute('a_Normal', CUBE_NORMALS);
const modelMat = ModelMatUniform('u_ModelMat', {
scale: 5,
rotate: [Math.PI / 4, 2, 1, 0],
});
const viewMat =
ViewMatUniform(
'u_ViewMat', /* eye */ [0, 0, 30], /* at */ [0, 0, 0], /* up */ [0, 1, 0]);
const perspectiveMat =
PerspectiveMatUniform(
'u_PerspectiveMat', /* fovy */ Math.PI / 3, /* aspect */ 1, /* near */ 1,
/* far */ 1e6);
const shader = Shader(vertexSrc, fragmentSrc, {
indices: CUBE_INDICES,
attributes: [aVertex, aNormal],
uniforms: [modelMat, viewMat, perspectiveMat],
mode: WebGLRenderingContext.TRIANGLES,
});
The resulting function, shader
, can be used to apply that shader to a render target
such as a canvas:
const canvas = document.querySelector('canvas');
shader(canvas);
The shader
function can also be called with a Texture2DUniform
to render
the shader to a texture with a WebGLFramebuffer
. The resulting texture can
later be sampled in other shaders. See examples/game_of_life
and
examples/ripple_effect
for some examples of how you can use this technique
for different things.
Examples
For more examples of how to use phixl for various WebGL things, see the examples
directory in this repository. They include:
 Rendering a 3D cube
 Rendering a 3D cube with a video texture
 Edge detection algorithm on webcam video
 GPU accelerated Conway's Game of Life
 Water ripple effect using the 2D wave equation
 Dynamic reflections using a CubeCamera
Documentation
Shader
The most important function in the phixl library, Shader
, is a factory function
that creates functions that apply a shader to a render target. It takes 3 arguments:

The vertex shader source code as a JS string

The fragment shader source code as a JS string

An object which should contain the following keys:
attributes
: An array of attributes for the shader. See the Attributes section below for what objects to use as elements for the array. This array must have at least one element.uniforms
: Optional. An array of uniforms for the shader. See the Uniforms section below for what objects to use as elements of this array. This array may be empty or omitted.indices
: Optional. If provided the shader will render usinggl.drawElements
instead ofgl.drawArrays
.mode
: Optional. Which mode WebGL will use to draw the vertices. The default isWebGLRenderingContext.TRIANGLE_STRIP
.clear
: Optional. Whether the shader should callgl.clear
. If the option is omitted then the value will be treated astrue
.viewport
: Optional. The viewport that WebGL should use when rendering the shader. It should be an array of 4 numbers. The elements will be used as arguments forgl.viewport
.
Attributes
This library provides an abstraction for sending attributes to shaders with just a function call. This library assumes that a shader's attributes are immutable and should not be changed once they are initialized.
All attributes take 2 arguments:

The name of the attribute in the shader

The data for the attribute.
The value for the second argument depends on the type of the attribute.
For all attributes except for matrix attributes, the argument should be
a Float32List
. These uniforms are:
FloatAttribute
Vec2Attribute
Vec3Attribute
Vec4Attribute
Matrix attributes
Unlike all other attributes, matrix attributes take an array of Float32List
as the second argument. Each element of the array is a vector attribute for each
column vector of the matrix. Matrix attributes are:
Mat2Attribute
Mat3Attribute
Mat4Attribute
Uniforms
This library also provides some abstractions for sending uniforms to WebGL shaders as well.
Unlike attributes, uniforms are not immutable, and can have their value changed.
Almost all uniforms have a set()
method for setting the uniforms data after
it is created and a data()
method for retrieving the current value of the data.
The set()
method can also be called with a callback with no arguments that
returns the uniform data. The callback will be invoked several times in phixl's
internals so it should not have side effects.
Below are a list of uniforms available to phixl users. The parameters of uniform functions and their behavior varies quite a bit more than attributes. All uniforms take the name of the uniform in the shader as their first argument. Below is a list of the different uniforms and what parameters they expect.
BooleanUniform
Sends a boolean value to a shader uniform. The second argument should be a number or boolean. Example usage:
const bool = BooleanUniform('u_Foo', true);
IntegerUniform
Sends an integer value to a shader uniform. The second argument should be a whole number. Example usage:
const int = IntegerUniform('u_Foo', 3);
FloatUniform
Sends a float value to a shader uniform. The second argument should be a number. Example usage:
const float = new FloatUniform('u_Foo', Math.PI);
Vector uniforms
Vector uniforms send a vector of float values to a shader uniform. The second argument should be an array of numbers. The size of the array depends on the dimension of the vector. Example usage:
const vec2 = Vec2Uniform('u_Foo', [1, 2]);
const vec3 = Vec3Uniform('u_Bar', [1, 2, 3]);
const vec4 = Vec4Uniform('u_Baz', [1, 2, 3, 4]);
Matrix uniforms
Matrix uniforms sends a matrix of float values to a shader uniform. The second
argument should be an array of numbers. The size of the array depends on the
dimension of the matrix. Example usage with
glmatrix
:
const {mat2, mat3, mat4} = require('glmatrix');
const mat2 = Mat2Uniform('u_Foo', mat2.create());
const mat3 = Mat3Uniform('u_Bar', mat3.create());
const mat4 = Mat4Uniform('u_Baz', mat4.create());
ModelMatUniform
The ModelMatUniform
senda a 4D matrix uniform for shaders meant to transform
model vertices into the world coordinates. The second argument is an optional
object with the following (each optional) keys:
const modelMat = ModelMatUniform('u_ModelMat', {
scale: 2, // Can also be an array with 1, 3, or 4 elements.
rotate: [
/* theta */ Math.PI / 2,
/* axis.x */ 2,
/* axis.y */ 1,
/* axis.z */ 0,
],
translate: [10, 10, 0],
});
The uniform computes the 4D model matrix which applies the corresponding combination of transformations, the scaling is applied first, then the rotation, and finally the translation.
The object returned by ModelMatUniform
has accessor methods which let you get
the different components of the model transformation:
modelMat.scaleMatrix(); // Gets the scale matrix as a 4D matrix.
modelMat.rotationMatrix(); // Gets the rotation matrix as a 3D matrix.
modelMat.translation(); // Gets the translation as a 3D vector.
The object returned by ModelMatUniform
has convenience methods which allow you
to change each individual part of the transformation:
modelMat.scale(3); // Applies this scale to the existing scale.
modelMat.setScale(3); // Resets the scale component of the matrix to the new value.
modelMat.rotate(Math.PI / 2, 1, 1, 0); // Applies the rotation to the existing rotation matrix.
modelMat.setRotation(Math.PI / 2, 1, 1, 0); // Sets the rotation matrix to apply only the provided rotation.
modelMat.translate(1, 2, 3); // Adds the new values to the existing translation vector.
modelMat.setTranslation(1, 2, 3); // Sets the translation vector to this new one.
NormalMatUniform
The NormalMatUniform
sends a 3D matrix uniform for transforming model normal vectors to
a shader. It takes an object returned by ModelMatUniform
as a second argument and computes
the resulting normal matrix automatically. Below is an example:
const modelMat = ModelMatUniform('u_ModelMat', ...);
const normalMat = NormalMatUniform('u_NormalMat', modelMat);
ViewMatUniform
The ViewMatUniform
sends a 4D matrix to a shader which transforms vertices from world coordinates
to the view coordinates of the scene's camera. It takes multiple arguments to compute the view matrix
and is based on glmatrix
's lookAt
function. Below is an example:
const viewMat =
ViewMatUniform(
'u_ViewMat', /* eye */ [0, 0, 30], /* at */ [0, 0, 0], /* up */ [0, 1, 0]);
The object returned by ViewMatUniform
has accessor methods which let you get the eye, at, or up
vectors that is used to compute the view matrix:
viewMat.eye();
viewMat.at();
viewMat.up();
The object also has setter methods for each vector as well:
viewMat.setEye(10, 0, 30);
viewMat.setAt(10, 0, 0);
viewMat.setUp(1, 10, 0);
As you may notice, the up vector does not need to be normalized, the object will do that for you when
it computes the view matrix. The up()
method will return the value passed to setUp
before
normalization.
PerspectiveMatUniform
The PerspectiveMatUniform
sends a 4D matrix to a shader which transforms vertices from the view
coordinates of the scene's camera to a coordinate system with linear perspective. It takes multiple
arguments to compute the perspective matrix and is based on gmatrix
's perspective
function.
Below is an example:
const perspectiveMat =
PerspectiveMatUniform(
'u_PerspectiveMat', /* fovy */ Math.PI / 3, /* aspect */ 1, /* near */ 1,
/* far */ 1e6);
The object returned by PerspectiveMatUniform
has accessor methods which let you get the
field of view (in radians), the widthtoheight aspect ratio, the near plane, and the far
plane that is used to compute the perspective matrix:
perspectiveMat.fovy();
perspectiveMat.aspect();
perspectiveMat.near();
perspectiveMat.far();
The object also has setter methods for each parameter for the perspective matrix:
perspectiveMat.setFovy(Math.PI / 4);
perspectiveMat.setAspect(canvas.width / canvas.height);
perspectiveMat.setNear(0.1);
perspectiveMat.setFar(1e4);
Texture2DUniform
The Texture2DUniform
sends a 2D texture to a shader in a sampler2D
uniform. The second argument is any object which can be used as the data
source for gl.texImage2D(...)
. Below are some examples of how you can
initialize a Texture2DUniform
:
const image = document.querySelector('img');
const imageTexture = Texture2DUniform('u_Foo', image);
const video = document.querySelector('video');
const videoTexture = Texture2DUniform('u_Bar', video);
const canvas = document.querySelector('canvas');
const canvasTexture = Texture2DUniform('u_Baz', canvas);
// Also could use ImageBitmap, ImageData, or OffscreenCanvas...
The objects returned by uniforms like Texture2DUniform
can be the argument
of functions returned by Shader
. This allows you to apply shaders to textures
which can be then used in other shaders. Below is an example:
const textureShader = Shader(...);
const texture = Texture2DUniform('u_Texture');
// We render the first shader to the texture.
textureShader(texture);
const shader = Shader(vertexSrc, fragmentSrc, {
// attributes, other options...
uniforms: [
texture,
// Other uniforms...
],
});
// The second shader renders to the canvas and can sample from the result
// of the first shader, textureShader.
shader(canvas);
CubeTextureUniform
The CubeTextureUniform
sends a cube texture to a shader in a samplerCube
uniform. The second argument to CubeTextureUniform
should be an object with
a key for each face of a cube: posx
, negx
, posy
, negy
, posz
, and negz
.
Each value of the object should be a data source for a 2D texture that you would
for gl.texImage2D
:
const cubeTexture = CubeTextureUniform('u_Texture', {
posx: rightImage,
negx: leftImage,
posy: topImage,
negy: bottomImage,
posz: frontImage,
negz: backImage,
});
Unlike Texture2DUniform
, the object returned by CubeTextureUniform
cannot be
used as the argument for a function returned by Shader
, though it may be nice
at some point to support this!
CubeCameraUniform
CubeCameraUniform
allows you to render a shader to a cube texture for things
like environment mapping and dynamic reflections. It should only be used for shaders
which have a view and perspective matrix, which is almost always present in 3D
scenes.
The arguments after the name of the uniform should be the position of the cube camera
in the scene as an array of numbers, the scene's ViewMatUniform
, and the scene's
PerspectiveMatUniform
.
For an example of how to use the CubeCameraUniform
for dynamic reflections, see
examples/cube_camera/src/main.js
.
License
This code is publicly available under an Apache2.0 license. See LICENSE for more information.