Side-by-side View of the Standard and a Custom Phong Material Shader

Extending a Shader

When working on a recent Three.js project, I wanted to extend one of the standard, built-in shaders like the MeshPhongMaterial shader. While it doesn’t seem like there is a super easy way to do that, there were some suggestions on StackOverflow. After futzing around for awhile, I created this customization on top Three.js’ Phong vertex shader for demonstration purposes. If you check out the link, you’ll see the cube on the left uses the standard Phong material shader, and the cube on right uses the custom one. The code is on GitHub.

In this post, I’ll describe the process in case you’re interested in trying it yourself.

Modifications

The first thing I did was modify the creating-a-scene tutorial from Three.js’ docs to use the standard Phong material instead of the standard basic material. The only catch is that the Phong shader requires lights to be present, so I added in an ambient light and a point light.

The GLSL code for the standard materials currently live in the src/renderers/shaders/ShaderLib directory of the Three.js repo. Among the files in that directory is meshphong_vert.glsl, which is the shader I wanted to customize. I copied the code from meshphong_vert.glsl and pasted it into a <script> tag in index.html so I could later extract it with JavaScript. Note that for a production application, it’d make a lot of sense to use your asset pipeline to import these custom shaders rather than sticking them in a <script> tag.

Next, I modified the Phong vertex shader. The first modification was to add a uniform and an attribute:

uniform float time;
attribute float offset;

The time uniform will be updated every frame with timestamps from requestAnimationFrame. The offset attribute will contain a random float between 0.0 and 1.0 that will be used to slightly offset individual vertices.

The second modification was to displace each vertex of the cube as a function of time and the offset described above. I did this by by modifying the coordinates of gl_Position (which is how the vertex shader tells WebGL the position of each vertex) :

float warp = time / 3000.0;
gl_Position.x *= abs(sin(warp + offset));
gl_Position.y *= abs(sin(warp + offset));
gl_Position.z *= abs(sin(warp + offset));

It’s important to put this code after the #include <project_vertex> line copied from the standard shader because project_vertex.glsl is where the standard Phong shader sets the original value for gl_Position.

Having placed the custom shader into index.html, it can now be used to shade the cube:

var customVertexShader = document.getElementById('js-custom-vertex-shader').textContent;
var customUniforms = THREE.UniformsUtils.merge([
  THREE.ShaderLib.phong.uniforms,
  { diffuse: { value: new THREE.Color(cubeColorHex) } },
  { time: { value: 0.0 } }
]);
var customMaterial = new THREE.ShaderMaterial({
  uniforms: customUniforms,
  vertexShader: customVertexShader,
  fragmentShader: THREE.ShaderLib.phong.fragmentShader,
  lights: true,
  name: 'custom-material'
});

First, the vertex shader is pulled out of the DOM. Next, the default Phong uniforms are copied out of ShaderLib. I manually set the diffuse uniform to the cube’s color and added the time uniform that will be updated with timestamps from requestAnimationFrame.

With the uniforms ready to go, the next step was to create the ShaderMaterial that takes the custom vertex shader as input. Since I’m not modifying the Phong fragment shader, I passed in the standard one from ShaderLib.

From here forward, the only departures from the standard Phong material scene is that, when using the custom shader, I set the offset attribute per vertex when first configuring the scene, and I update the time uniform every frame. Check out the code here to see how I went about doing that.

Long Story Short

Long story short, it’s totally possible to modify Three.js’ built in shaders, albeit with some vigorous copy/pasting. Please let me know if you found another way to do it that feels less hacky.