Pencil Shader Development

Whilst undergoing my university final project, I decided to undergo coding a pencil shader in only a week! (Why? because I’m an over-ambitious fool)

As of writing this, it remains simply a dream.

I had previously watched videos on building shaders in Unreal Engine, and felt it a task worth completing, if only I could make it work in 3JS.

A quick google search found very few pencil shaders. One especially being a WebGL Pencil Shader (you can view the code in-page) which looks rather… unpencilly and very crosshatchy. In my personal work, I have a very distinct ink drawing style, which I wanted to emulate in a shader.

I found this youtube course which I could use as my basis. My main hurdle was that I simply didn’t know how to integrate the shader code, and the 3D code together.

I started off by setting up my javascript in the same way I would start a regular 3JS page – camera, orbit controls, geometry, render.

Then I took the shader code created in lesson 2 of the course, and stitched that into my code. The result was this:

Taking the shaders from the WebGL Pencil Shader Example, I was able to get this result:

This perfectly combines the shader with the code that I wrote.

Issues:

  • cursor position changes the light direction
  • appearance of the lines.

By changing the rotation coordinates of the lines, I’m able to lay them on top in a similar fashion to how I render my drawings.

Issues:

  • lines are too uniform
  • not pencil-like
  • lines still follow the cursor.

By removing the mouse variable, I was able to stop the light direction from following the mouse. However, some code once removed broke the shader, so I left the remnants in.

const uniforms = {
  u_color: { value: new THREE.Color(0xff0000) },
  u_time: { value: 0.0 },
//  u_mouse: { value:{ x:5, y:5 }}, // removes the mouse being used to change light direction
  u_resolution: { value:{ x:0, y:0 }}
}

Then, through playing with the settings of light direction, I was able to adjust the angle and contrast to my liking

//         mouse var changes nothing    changes light direction                 if prev val is low, does nothing. If high, adjusts contrast
vec3 light_direction = vec3((u_mouse - 0.3 * u_resolution) / min_resolution, -0.99);

Issues:

  • Lines too uniform
  • Lines not pencil-like

These problems proved to be quite difficult for me, this being my first time trying to code a shader. I spent a long few hours playing with values and trying to use randomness.

My thinking is as follows – if I can randomise the angles and sizes slightly, it’ll look more hand drawn. I also really want to have a more pencil-like texture; this, I currently think can be achieved using Noise. I watched this video on using noise to blend textures using glsl and javascript.

Another thing I want to be able to do is blend line weight, where the line will subtly be thinner and thicker in areas, as if the hand drawing it was adding variable pressure

I think I will also need to vary the line seperation, so that the gaps feel more natural and less uniform

After trying almost every method of importing the full model, including glb, fbx, and obj… I finally managed to get the model to load with the shader material.

Currently, it looks great from the top, but terrible front on. This is because the scale has changed. The model is so much bigger compared to the torus knot

At this point I think it really needs an outline shader so that you can clearly see the mesh in its entirety

In an effort to not have to code all that (lol) I did the tried and tested process of duplicating your object, and flipping the normals.

However, this resulted in this. While a cool rim light effect, I don’t think this quite worked 🙁

code here

const uniforms = {
  u_color: { value: new THREE.Color(0xff0000) },
  u_time: { value: 0.0 },
//  u_mouse: { value:{ x:5, y:5 }}, // removes the mouse being used to change light direction
  u_resolution: { value:{ x:0, y:0 }}
}

const material = new THREE.ShaderMaterial( {
  uniforms: uniforms,
  vertexShader: vshader,
  fragmentShader: fshader
} );

const outline = new THREE.MeshPhongMaterial();
outline.color.setHSL(0, 1, .5);  // red
outline.flatShading = true;

const fbxLoader = new FBXLoader();

fbxLoader.load('Model_Low.fbx', (object) => {
    ////Now we find each Mesh...
    object.traverse( function ( child ) {
        if ( child instanceof THREE.Mesh ) {

             ////...and we replace the material with our custom one
            child.material = material;
            child.material.side = THREE.DoubleSide;
        }
    });
  object.position.set(0, -10, 0)
  object.scale.set(0.07, 0.07, 0.07)
  scene.add(object);
});

fbxLoader.load('Model_Low.fbx', (outlineobject) => {
    ////Now we find each Mesh...
    outlineobject.traverse( function ( child ) {
        if ( child instanceof THREE.Mesh ) {

             ////...and we replace the material with our custom one
            child.material = outline;
        }
    });
  outlineobject.position.set(0, -10, 0)
  outlineobject.scale.set(0.07, 0.07, 0.07)
  scene.add(outlineobject);
});

Leave a Reply

Your email address will not be published. Required fields are marked *

*