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 glslify-loader
and raw-loader
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', [0, 0, 30], [0, 0, 0], [0, 1, 0]);
const perspectiveMat =
PerspectiveMatUniform(
'u_PerspectiveMat', Math.PI / 3, 1, 1,
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 using gl.drawElements
instead of
gl.drawArrays
.mode
: Optional. Which mode WebGL will use to draw the vertices. The default is
WebGLRenderingContext.TRIANGLE_STRIP
.clear
: Optional. Whether the shader should call gl.clear
. If the option is omitted then
the value will be treated as true
.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 for gl.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 Float32Array
. These uniforms are:
FloatAttribute
Vec2Attribute
Vec3Attribute
Vec4Attribute
Matrix attributes
Unlike all other attributes, matrix attributes take an array of Float32Array
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
gl-matrix
:
const {mat2, mat3, mat4} = require('gl-matrix');
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,
rotate: [
Math.PI / 2,
2,
1,
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();
modelMat.rotationMatrix();
modelMat.translation();
The object returned by ModelMatUniform
has convenience methods which allow you
to change each individual part of the transformation:
modelMat.scale(3);
modelMat.setScale(3);
modelMat.rotate(Math.PI / 2, 1, 1, 0);
modelMat.setRotation(Math.PI / 2, 1, 1, 0);
modelMat.translate(1, 2, 3);
modelMat.setTranslation(1, 2, 3);
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 gl-matrix
's lookAt
function. Below is an example:
const viewMat =
ViewMatUniform(
'u_ViewMat', [0, 0, 30], [0, 0, 0], [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 g-matrix
's perspective
function.
Below is an example:
const perspectiveMat =
PerspectiveMatUniform(
'u_PerspectiveMat', Math.PI / 3, 1, 1,
1e6);
The object returned by PerspectiveMatUniform
has accessor methods which let you get the
field of view (in radians), the width-to-height 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);
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');
textureShader(texture);
const shader = Shader(vertexSrc, fragmentSrc, {
uniforms: [
texture,
],
});
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 Apache-2.0 license. See LICENSE for more
information.