Working on a Custom Shader
We are aiming to create a very unique and identifiable art style for our game. Marco is working hard to develop the style and I have been tasked with writing a custom shader which can efficiently reproduce our art style without us needing to create endless textures and lighting (to increase performance and lower network load).
Shaders in Three.js
first I want to talk a little bit about the environment. In Three.js, there are several useful workflows setup to help with custom shader creation. For the most part, these workflows come in the form of uniforms. When defining a custom material, you can also define custom and pre-existing uniforms. In the code below I’ve defined a custom material with my own shader code and the three.js uniforms for lighting:
1 2 3 4 5 6 7 8 9 10 11 |
//load shader this.toonMat = new THREE.ShaderMaterial( { uniforms: THREE.UniformsLib['lights'], attributes: {}, vertexShader: this.toonVert, fragmentShader: this.toonFrag, lights: true }); |
This process then allows me to use the lights in the scene in my fragment shader. As mentioned before, the uniform lib provides uniforms and constants to the shader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
precision highp float; varying vec3 vObjPos; varying vec3 vWorldPos; varying vec3 vWorldNormal; #if MAX_POINT_LIGHTS > 0 uniform vec3 pointLightColor[MAX_POINT_LIGHTS]; uniform vec3 pointLightPosition[MAX_POINT_LIGHTS]; uniform float pointLightDistance[MAX_POINT_LIGHTS]; #endif #if MAX_DIR_LIGHTS > 0 uniform vec3 directionalLightColor[MAX_DIR_LIGHTS]; uniform vec3 directionalLightDirection[MAX_DIR_LIGHTS]; #endif uniform vec3 ambientLightColor; |
Based on the beginnings of our art style, I also knew that I would need to generate some kind of noise in the shader. Unfortunately, GLSL does not natively support any kind of random function, but I did find a function which generates pseudo-random noise. Essentially, this is a function which is used because of its lack of repetition and pattern. The original source of this function is unknown to me, but it works well for our purposes. I also had to modify the function so that it supported 3D noise:
1 2 3 4 5 6 7 |
//from the web, source unknown float rand(vec3 v) { return 0.5 + 0.5 * fract(sin(dot(v.xyz, vec3(12.9898, 78.233, 98.764))) * 43758.5453); } |
I apply the noise in object space so that if the object is moving around the scene, the texture does not change on its surface. Otherwise the rest of this code is just setup to handle basic lighting from ambient, directional, and point lights.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
void main() { vec4 sumLights = vec4(0.0, 0.0, 0.0, 1.0); //point lights vec4 sumPointLights = vec4(0.0, 0.0, 0.0, 1.0); #if MAX_POINT_LIGHTS > 0 for(int i = 0; i < MAX_POINT_LIGHTS; i++) { vec3 dir = normalize(vWorldPos - pointLightPosition[i]); sumPointLights.rgb += clamp(dot(-dir, vWorldNormal), 0.0, 1.0) * pointLightColor[i]; } #endif //directional lights vec4 sumDirLights = vec4(0.0, 0.0, 0.0, 1.0); #if MAX_DIR_LIGHTS > 0 for(int i = 0; i < MAX_DIR_LIGHTS; i++) { vec3 dir = directionalLightDirection[i]; sumDirLights.rgb += clamp(dot(-dir, vWorldNormal), 0.0, 1.0) * directionalLightColor[i]; } #endif //take ambient light, add highlight if point sum big enough sumLights = sumPointLights + sumDirLights; //sumLights = vec4(ambientLightColor, 1.0) + floor( sumLights * vec4(5, 5, 5, 1)) * vec4(0.2, 0.2, 0.2, 1); sumLights = vec4(ambientLightColor, 1.0) + sumLights; gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * sumLights - 0.05 + 0.1 * rand( vec3( floor(vObjPos.x * 100.0), floor(vObjPos.y * 100.0), floor(vObjPos.z * 100.0) ) * 0.01); } |