Customizing Three.js Phong Shader for Animated 3D Dice

Customizing Three.js Phong Shader for Animated 3D Dice

One of our core goals for is to build a platform to enable the community to create amazing 3d dice that they can share with the world.

Computer graphics has always been a passion of mine. A friend and I somehow convinced the high school shop teacher to buy 3ds Max, and "teach" us 3d modeling. Fast forward and I got a degree in computer graphics. This is kinda my thing.


It's so many other people's things too. And they are 100x better than me at beautiful 3d everything. So while I do think the dice we have made are cool, we know the community will make them cooler.

🎲 Here is how the dice are made.

We are built on top of a javascript library called three.js this is a truly amazing graphics package built on top of WebGL. For the uninitiated, WebGL allows web applications to unlock the power of a GPU.

The basic building blocks of our dice is a 3d model (duh) and a three.js MeshShaderMaterial. This allows us to write our own custom shader code is GLSL. We started with the standard phong shader used the MeshPhongMaterial:

Our shader code is written in GLSL 3.0 ES in a slightly modified form. Three.js has their own custom preprocessor which implements the #include directive. We make heavy use of it. This code will not work outside Three.js
uniform vec3 diffuse;
uniform vec3 emissive;
uniform vec3 specular;
uniform float shininess;
uniform float opacity;
#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <alphatest_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <emissivemap_pars_fragment>
#include <envmap_common_pars_fragment>
#include <envmap_pars_fragment>
#include <cube_uv_reflection_fragment>
#include <fog_pars_fragment>
#include <bsdfs>
#include <lights_pars_begin>
#include <normal_pars_fragment>
#include <lights_phong_pars_fragment>
#include <shadowmap_pars_fragment>
#include <bumpmap_pars_fragment>
#include <normalmap_pars_fragment>
#include <specularmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
	#include <clipping_planes_fragment>
	vec4 diffuseColor = vec4( diffuse, opacity );
	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
	vec3 totalEmissiveRadiance = emissive;
	#include <logdepthbuf_fragment>
	#include <map_fragment>
	#include <color_fragment>
	#include <alphamap_fragment>
	#include <alphatest_fragment>
	#include <specularmap_fragment>
	#include <normal_fragment_begin>
	#include <normal_fragment_maps>
	#include <emissivemap_fragment>
	// accumulation
	#include <lights_phong_fragment>
	#include <lights_fragment_begin>
	#include <lights_fragment_maps>
	#include <lights_fragment_end>
	// modulation
	#include <aomap_fragment>
	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
	#include <envmap_fragment>
	#include <output_fragment>
	#include <tonemapping_fragment>
	#include <encodings_fragment>
	#include <fog_fragment>
	#include <premultiplied_alpha_fragment>
	#include <dithering_fragment>
Phong Fragment Shader

The Phong shading model has 2 key components that we are making use of to build our dice. The diffuse color and the texture. You can see those defined as uniform at the top of the fragment shader above. The way this works is the diffuse color is the base color of the object. This was traditionally a grayscale image of varying intensity use to simulate the texture of a surface. This is multiplied by the diffuse color (using linear color math) to create variations in the surface color.

In the decades since there have been developed more advanced ways to model the physical texture of objects (bump mapping, normal mapping) but terminology is sticky, and so here we are

In order to get the numbers on the dice we used a technique called diffuse mapping, where instead of using the texture to simulate... texture, we use it to directly color the faces by setting the diffuse color to 0 and thus ignoring the multiplication.

Diffuse map for the "Red" theme

πŸ™ˆ Adding the hidden roll feature

The hidden roll feature prompted us to take the first step into using the power of custom shaders. Hidden rolls are a staple of roleplaying. Some GMs love them, some hate them, one I played with would hide every roll he made. So this feature was a must.

We considered multiple ways this could be embodies on Doing something with extra shader passes, like blacking them out, or blurring them at the same time we do the outlining. But we decided to put it in the shader to eventually give you, the potential dice creator, the most power. Imagine a die you roll, all faces ? then when then at reveal time it morphs into the correct number. Or a fire burns on the face leaving the number behind in soot! The ideas are limitless this way, but for today you will have to settle on a simple fade in.

Lets look at the code:

@@ -13,6 +13,7 @@ uniform vec3 emissive;
 uniform vec3 specular;
 uniform float shininess;
 uniform float opacity;
+uniform float hiddenness;
 #include <common>
 #include <packing>
 #include <dithering_pars_fragment>
@@ -45,8 +46,15 @@ void main() {
 	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
 	vec3 totalEmissiveRadiance = emissive;
 	#include <logdepthbuf_fragment>
-	#include <map_fragment>
-	#include <color_fragment>
+	// copy paste of map_fragment but modified to use hiddenness to hide texture
+	#ifdef USE_MAP
+        vec4 sampledDiffuseColor = texture2D( map, vUv );
+        diffuseColor *= mix(sampledDiffuseColor,texture2D(map, vec2(0.0,0.0)),hiddenness);
+    #endif
 	#include <alphamap_fragment>
 	#include <alphatest_fragment>
 	#include <specularmap_fragment>

We added a uniform called hiddeness to our shader. In our animation loop we pass this in to all dice, a 1 means fully hidden, and a 0 means fully unhidden. When you click the "unhide" button in the interface, our animation loop will tween this from 1 to 0. And in your shader you can do whatever you like!

Our original dice all had white defined as their diffuse color, and in true lazy-programmers-are-the-best-programmers style I didn't feel like sampling all the textures and updating the definitions. So I assumed the color at 0,0 in the texture was what the diffuse should have been and coded that into the shader: texture2D(map, vec2(0.0,0.0). Then we use mix function to combine this faked diffuse color with the color on the diffuse map in the proportion of hiddeness. Mix is usually used to do alpha blending, you can think of what we are doing as layering the diffuse over the texture with hiddeness as your alpha.

And this is what you get:


This is only the very beginning of what customizations could be done on the dice by implementing custom shader code. With the release of our editor you can customize the dice we have already made, but with the API you can upload your own shaders. I can't wait to see what you all come up with!

If you're interested in joining our growing community, join our Discord, follow us on Twitter, join the subreddit, and stay tuned for more updates.