Ink 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)
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.
Skip along the progress….
- WebGL basic pencil shader
- The lines are now vertical!
- Imported character model
- Outline attempt 1 (inverted hull)
- Perlin Noise implementation
- Using the UVs to draw lines
- Ink Shader first success!
- Sobel Filter (Edge detection)
- Inverted hull part 2 (partial success)
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:

Show JavaScript Code
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
//import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
//////////////////////////////////////////////////////////////////////////////////////////////
/// shaders //////////////////////////////////////////////////////////////////////////////////
const vshader = `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`
const fshader = `
uniform vec3 u_color;
uniform vec2 u_mouse;
uniform vec2 u_resolution;
uniform float u_time;
void main (void)
{
vec3 color = vec3(u_mouse.x/u_resolution.x, 0.0, u_mouse.y/u_resolution.y);
//color = vec3((sin(u_time)+1.0)/2.0, 0.0, (cos(u_time)+1.0)/2.0);
gl_FragColor = vec4(color, 1.0);
}
`
//////////////////////////////////////////////////////////////////////////////////////////////
/// setup ////////////////////////////////////////////////////////////////////////////////////
const loader = new GLTFLoader();
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
const scene = new THREE.Scene();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// Camera - fov, aspect, near, far
const camera = new THREE.PerspectiveCamera(
45, //fov
window.innerWidth / window.innerHeight, //aspect
2, //near clip
10000 //far clip
);
// x y z
//camera.position.set(220, 260, 900);
camera.position.z = 100;
// orbit controls //
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
renderer.render(scene, camera);
const clock = new THREE.Clock();
//////////////////////////////////////////////////////////////////////////////////////////////
/// geometry /////////////////////////////////////////////////////////////////////////////////
const geometry = new THREE.TorusKnotGeometry(
3.5, //radius
2.2, //tube radius
62, //tubularSegments
10, //radialSegments
2, //p
3 //q
);
const uniforms = {
u_color: { value: new THREE.Color(0xff0000) },
u_time: { value: 0.0 },
u_mouse: { value:{ x:0.0, y:0.0 }},
u_resolution: { value:{ x:0, y:0 }}
}
const material = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: vshader,
fragmentShader: fshader
} );
const torus = new THREE.Mesh( geometry, material );
scene.add( torus );
scene.background = new THREE.Color(0x333);
//////////////////////////////////////////////////////////////////////////////////////////////
/// Rendering ////////////////////////////////////////////////////////////////////////////////
onWindowResize();
if ('ontouchstart' in window){
document.addEventListener('touchmove', move);
}else{
window.addEventListener( 'resize', onWindowResize, false );
document.addEventListener('mousemove', move);
}
function move(evt){
uniforms.u_mouse.value.x = (evt.touches) ? evt.touches[0].clientX : evt.clientX;
uniforms.u_mouse.value.y = (evt.touches) ? evt.touches[0].clientY : evt.clientY;
}
console.log(scene);
animate();
function onWindowResize( event ) {
const aspectRatio = window.innerWidth/window.innerHeight;
let width, height;
if (aspectRatio>=1){
width = 1;
height = (window.innerHeight/window.innerWidth) * width;
}else{
width = aspectRatio;
height = 1;
}
camera.left = -width;
camera.right = width;
camera.top = height;
camera.bottom = -height;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
uniforms.u_resolution.value.x = window.innerWidth;
uniforms.u_resolution.value.y = window.innerHeight;
}
function animate() {
requestAnimationFrame(animate);
uniforms.u_time.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
animate();
Taking the shaders from the WebGL Pencil Shader Example, I was able to get this result:

Show JavaScript Code
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
//import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
//////////////////////////////////////////////////////////////////////////////////////////////
/// shaders //////////////////////////////////////////////////////////////////////////////////
// Shader code adapted from --> https://webgl-shaders.com/pencil-example.html
const vshader = `
#define GLSLIFY 1
// Common varyings
varying vec3 v_position;
varying vec3 v_normal;
/*
* The main program
*/
void main() {
// Save the varyings
v_position = position;
v_normal = normalize(normalMatrix * normal);
// Vertex shader output
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
const fshader = `
#define GLSLIFY 1
// Common uniforms
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
uniform float u_frame;
// Common varyings
varying vec3 v_position;
varying vec3 v_normal;
/*
* Returns a rotation matrix for the given angle
*/
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
/*
* Calculates the diffuse factor produced by the light illumination
*/
float diffuseFactor(vec3 normal, vec3 light_direction) {
float df = dot(normalize(normal), normalize(light_direction));
if (gl_FrontFacing) {
df = -df;
}
return max(0.0, df);
}
/*
* Returns a value between 1 and 0 that indicates if the pixel is inside the horizontal line
*/
float horizontalLine(vec2 pixel, float y_pos, float width) {
return 1.0 - smoothstep(-1.0, 1.0, abs(pixel.y - y_pos) - 0.5 * width);
}
/*
* The main program
*/
void main() {
// Use the mouse position to define the light direction
float min_resolution = min(u_resolution.x, u_resolution.y);
vec3 light_direction = -vec3((u_mouse - 0.5 * u_resolution) / min_resolution, 0.5);
// Calculate the light diffusion factor
float df = diffuseFactor(v_normal, light_direction);
// Move the pixel coordinates origin to the center of the screen
vec2 pos = gl_FragCoord.xy - 0.5 * u_resolution;
// Rotate the coordinates 20 degrees
pos = rotate(radians(20.0)) * pos;
// Define the first group of pencil lines
float line_width = 7.0 * (1.0 - smoothstep(0.0, 0.3, df)) + 0.5;
float lines_sep = 16.0;
vec2 grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
float line_1 = horizontalLine(grid_pos, lines_sep / 2.0, line_width);
grid_pos.y = mod(pos.y + lines_sep / 2.0, lines_sep);
float line_2 = horizontalLine(grid_pos, lines_sep / 2.0, line_width);
// Rotate the coordinates 50 degrees
pos = rotate(radians(-50.0)) * pos;
// Define the second group of pencil lines
lines_sep = 12.0;
grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
float line_3 = horizontalLine(grid_pos, lines_sep / 2.0, line_width);
grid_pos.y = mod(pos.y + lines_sep / 2.0, lines_sep);
float line_4 = horizontalLine(grid_pos, lines_sep / 2.0, line_width);
// Calculate the surface color
float surface_color = 1.0;
surface_color -= 0.8 * line_1 * (1.0 - smoothstep(0.5, 0.75, df));
surface_color -= 0.8 * line_2 * (1.0 - smoothstep(0.4, 0.5, df));
surface_color -= 0.8 * line_3 * (1.0 - smoothstep(0.4, 0.65, df));
surface_color -= 0.8 * line_4 * (1.0 - smoothstep(0.2, 0.4, df));
surface_color = clamp(surface_color, 0.05, 1.0);
// Fragment shader output
gl_FragColor = vec4(vec3(surface_color), 1.0);
}
`
//////////////////////////////////////////////////////////////////////////////////////////////
/// setup ////////////////////////////////////////////////////////////////////////////////////
const loader = new GLTFLoader();
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
const scene = new THREE.Scene();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// Camera - fov, aspect, near, far
const camera = new THREE.PerspectiveCamera(
45, //fov
window.innerWidth / window.innerHeight, //aspect
2, //near clip
10000 //far clip
);
// x y z
//camera.position.set(220, 260, 900);
camera.position.z = 100;
// orbit controls //
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
renderer.render(scene, camera);
const clock = new THREE.Clock();
//////////////////////////////////////////////////////////////////////////////////////////////
/// geometry /////////////////////////////////////////////////////////////////////////////////
const geometry = new THREE.TorusKnotGeometry(
3.5, //radius
2.2, //tube radius
62, //tubularSegments
10, //radialSegments
2, //p
3 //q
);
const uniforms = {
u_color: { value: new THREE.Color(0xff0000) },
u_time: { value: 0.0 },
u_mouse: { value:{ x:0.0, y:0.0 }},
u_resolution: { value:{ x:0, y:0 }}
}
const material = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: vshader,
fragmentShader: fshader
} );
const torus = new THREE.Mesh( geometry, material );
scene.add( torus );
scene.background = new THREE.Color(0x333);
//////////////////////////////////////////////////////////////////////////////////////////////
/// Rendering ////////////////////////////////////////////////////////////////////////////////
onWindowResize();
if ('ontouchstart' in window){
document.addEventListener('touchmove', move);
}else{
window.addEventListener( 'resize', onWindowResize, false );
document.addEventListener('mousemove', move);
}
function move(evt){
uniforms.u_mouse.value.x = (evt.touches) ? evt.touches[0].clientX : evt.clientX;
uniforms.u_mouse.value.y = (evt.touches) ? evt.touches[0].clientY : evt.clientY;
}
console.log(scene);
animate();
function onWindowResize( event ) {
const aspectRatio = window.innerWidth/window.innerHeight;
let width, height;
if (aspectRatio>=1){
width = 1;
height = (window.innerHeight/window.innerWidth) * width;
}else{
width = aspectRatio;
height = 1;
}
camera.left = -width;
camera.right = width;
camera.top = height;
camera.bottom = -height;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
uniforms.u_resolution.value.x = window.innerWidth;
uniforms.u_resolution.value.y = window.innerHeight;
}
function animate() {
requestAnimationFrame(animate);
uniforms.u_time.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
animate();
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.

Rather than simple straight lines, noise should give the lines a bit of a wiggle, making it look more hand drawn.
Here is a really great shader example (which I cannot implement for the life of me)
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.
See the code that worked!
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 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(.07, .07, .07)
scene.add(object);
});


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. (Don’t worry I’ll try again later)

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);
});
Current code [23/03]
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
//import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
//import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
//////////////////////////////////////////////////////////////////////////////////////////////
/// shaders //////////////////////////////////////////////////////////////////////////////////
// Shader code adapted from --> https://webgl-shaders.com/pencil-example.html
const vshader = `
#define GLSLIFY 1
// Common varyings
varying vec3 v_position;
varying vec3 v_normal;
/*
* The main program
*/
void main() {
// Save the varyings
v_position = position;
v_normal = normalize(normalMatrix * normal);
// Vertex shader output
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
const fshader = `
#define GLSLIFY 1
// Common uniforms
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
uniform float u_frame;
// Common varyings
varying vec3 v_position;
varying vec3 v_normal;
/*
* Returns a rotation matrix for the given angle
*/
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
/*
* Calculates the diffuse factor produced by the light illumination
*/
float diffuseFactor(vec3 normal, vec3 light_direction) {
float df = dot(normalize(normal), normalize(light_direction));
if (gl_FrontFacing) {
df = -df;
}
return max(0.0, df); //upper bound is 0.9 (completely white), has to be a float, minuses are inconsequential
}
/*
* Returns a value between 1 and 0 that indicates if the pixel is inside the horizontal line
*/
float horizontalLine(vec2 pixel, float y_pos, float width) {
return 1.0 - smoothstep(-1.0, 1.0, abs(pixel.y - y_pos) - 0.9 * width);
}
/*
* The main program
*/
void main() {
// Use the mouse position to define the light direction
///////////////////////////////////////////////////////////////////////////////
float min_resolution = min(u_resolution.x, u_resolution.y);
// 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);
// Calculate the light diffusion factor
float df = diffuseFactor(v_normal, light_direction) * 0.98; // multiply by value to get contrast
// Move the pixel coordinates origin to the center of the screen
vec2 pos = gl_FragCoord.xy - 0.5 * u_resolution;
//// Rotate the coordinates //////////////////////////////////////////////////////////////////////////
pos = rotate(radians(90.0)) * pos;
////////////// Define the first group of pencil lines
// smooth step (edge, edge, value being tested) (essentially a ramp)
// less than lower bound = 0, in between is a gradient between both values, greater than upperbound = 1
// shadow size factor (shadow size bounds, diffusion factor)
float line_width = 2.0 * (1.0 - smoothstep(0.0, 0.5, df)) + 0.4; // fades
/////////////////////////////
float lines_sep = 4.0;
/////////////////////////////
vec2 grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
grid_pos.y = mod(pos.y + lines_sep / 2.0, lines_sep);
float line_1 = horizontalLine(grid_pos, lines_sep / 2.0, line_width); // num before line width makes it more/less bold (higher for more comic styles)
float line_2 = horizontalLine(grid_pos, lines_sep / 1.0, line_width);
//// Rotate the coordinates //////////////////////////////////////////////////////////////////////////
pos = rotate(radians(-0.5)) * pos;
///////////// Define the second group of pencil lines
lines_sep = 7.0;
////////////////////////////
grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
float line_3 = horizontalLine(grid_pos, lines_sep / 2.0, line_width );
grid_pos.y = mod(pos.y + lines_sep / 0.0, lines_sep);
float line_4 = horizontalLine(grid_pos, lines_sep / 1.0, line_width * 0.7);
// Calculate the surface color
float surface_color = 0.9;
surface_color -= 1.0 * line_1 * (1.0 - smoothstep(0.7, 0.9, df*0.9));
surface_color -= 0.8 * line_2 * (1.0 - smoothstep(0.35, 0.5, df));
surface_color -= 0.8 * line_3 * (1.0 - smoothstep(0.6, 0.65, df));
surface_color -= 0.8 * line_4 * (0.8 - smoothstep(0.3, 0.4, df));
surface_color = clamp(surface_color, 0.15, 1.0);
// Fragment shader output
gl_FragColor = vec4(vec3(surface_color), 0.98);
}
`
//////////////////////////////////////////////////////////////////////////////////////////////
/// setup ////////////////////////////////////////////////////////////////////////////////////
//const loader = new GLTFLoader();
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
const scene = new THREE.Scene();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// Camera - fov, aspect, near, far
const camera = new THREE.PerspectiveCamera(
45, //fov
window.innerWidth / window.innerHeight, //aspect
2, //near clip
10000 //far clip
);
// x y z
camera.position.set(0, 0, 10);
//camera.position.y = 50;
//camera.position.z = 20;
// orbit controls //
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
renderer.render(scene, camera);
const clock = new THREE.Clock();
//////////////////////////////////////////////////////////////////////////////////////////////
/// geometry /////////////////////////////////////////////////////////////////////////////////
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 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(.07, .07, .07)
scene.add(object);
});
scene.background = new THREE.Color(0x77AA99);
//////////////////////////////////////////////////////////////////////////////////////////////
/// Rendering ////////////////////////////////////////////////////////////////////////////////
onWindowResize();
console.log(scene);
animate();
function onWindowResize( event ) {
const aspectRatio = window.innerWidth/window.innerHeight;
let width, height;
if (aspectRatio>=1){
width = 1;
height = (window.innerHeight/window.innerWidth) * width;
}else{
width = aspectRatio;
height = 1;
}
//camera.left = -width;
//camera.right = width;
//camera.top = height;
//camera.bottom = -height;
//camera.updateProjectionMatrix();
uniforms.u_resolution.value.x = window.innerWidth;
uniforms.u_resolution.value.y = window.innerHeight;
}
function animate() {
requestAnimationFrame(animate);
uniforms.u_time.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
animate();
After a lot of struggling, I decided to go back to basics so I could learn GLSL without worrying about ruining the code I had already.
A lot of the struggle was me trying to figure out how to use noise. I for some reason could just not use a noise texture, so I used a function that coded Perlin Noise
Which you can view here (Perlin Noise)
// Classic Perlin 3D Noise
// by Stefan Gustavson
// https://github.com/visionary-3d/noise-collection/blob/main/glsl/perlin.glsl
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
float noise(vec3 P){
vec3 Pi0 = floor(P); // Integer part for indexing
vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
Pi0 = mod(Pi0, 289.0);
Pi1 = mod(Pi1, 289.0);
vec3 Pf0 = fract(P); // Fractional part for interpolation
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
vec4 iy = vec4(Pi0.yy, Pi1.yy);
vec4 iz0 = Pi0.zzzz;
vec4 iz1 = Pi1.zzzz;
vec4 ixy = permute(permute(ix) + iy);
vec4 ixy0 = permute(ixy + iz0);
vec4 ixy1 = permute(ixy + iz1);
vec4 gx0 = ixy0 / 7.0;
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
gx0 = fract(gx0);
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
vec4 sz0 = step(gz0, vec4(0.0));
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
vec4 gx1 = ixy1 / 7.0;
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
gx1 = fract(gx1);
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
vec4 sz1 = step(gz1, vec4(0.0));
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
g000 *= norm0.x;
g010 *= norm0.y;
g100 *= norm0.z;
g110 *= norm0.w;
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
g001 *= norm1.x;
g011 *= norm1.y;
g101 *= norm1.z;
g111 *= norm1.w;
float n000 = dot(g000, Pf0);
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
float n111 = dot(g111, Pf1);
vec3 fade_xyz = fade(Pf0);
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
return 2.2 * n_xyz;
}
This got me this outcome! ⬇️

I then tried to experiment with using the UVs to create lines. While it worked, it wasn’t at all what I needed.



Lines Code here:
void main() {
vec2 uv = vUv;
gl_FragColor = vec4(vec3(step(0.5, fract(uv.x * 10.0))), 1.);
}
While I liked the original code, it wasn’t as amenable to change as I’d have liked. Using the Book of Shaders, I used this “Wood Texture” to figure out how to use noise and lines.

Code
///////////////////////////////
// lines //////////////////////
float random (in vec2 st) {
return fract(sin(dot(st.xy,
vec2(12.9898,78.233)))
* 43758.5453123);
}
// Value noise by Inigo Quilez - iq/2013
// https://www.shadertoy.com/view/lsf3WH
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
vec2 u = f*f*(3.0-2.0*f);
return mix( mix( random( i + vec2(0.0,0.0) ),
random( i + vec2(1.0,0.0) ), u.x),
mix( random( i + vec2(0.0,1.0) ),
random( i + vec2(1.0,1.0) ), u.x), u.y);
}
mat2 rotate2d(float angle){
return mat2(cos(angle),-sin(angle),
sin(angle),cos(angle));
}
float lines(in vec2 pos, float b){
float scale = 10.0;
pos *= scale;
return smoothstep(0.0,
.5+b*.5,
abs((sin(pos.x*3.1415)+b*2.0))*.5);
}
/*
* The main program
*/
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st.y *= u_resolution.y/u_resolution.x;
vec2 pos = st.yx*vec2(10.,3.);
float pattern = pos.x;
// Add noise
pos = rotate2d( noise(pos) ) * pos;
// Draw lines
pattern = lines(pos,.5);
gl_FragColor = vec4(vec3(pattern),1.0);
// Fragment shader output
//gl_FragColor = vec4(vec3(surface_color), 1.0);
}
The only issue here is that it does not respond to light, and merely uses the object as a mask.
100 open tabs laters, and a lot of frustration, I got something that looked really close to what I wanted!!

Code
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
//import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
//import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
//import noise64 from "./images/smallNoise.png"
//////////////////////////////////////////////////////////////////////////////////////////////
/// shaders //////////////////////////////////////////////////////////////////////////////////
// Shader code adapted from --> https://webgl-shaders.com/pencil-example.html
const vshader = `
#define GLSLIFY 1
// Common varyings
varying vec3 v_position;
varying vec3 v_normal;
varying vec2 vUv;
/*
* The main program
*/
void main() {
// Save the varyings
v_position = position;
v_normal = normalize(normalMatrix * normal);
// Vertex shader output
// model matrix -> position, scale, rotation of model
// view matrix -> position, orientation of camera
// projection matrix -> projects object onto screen ( aspect ratio and perspective )
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
const fshader = `
#define GLSLIFY 1
uniform vec2 u_resolution;
uniform vec2 u_mouse;
varying vec3 v_position;
varying vec3 v_normal;
////////////////////////////////////
// noise ///////////////////////////
// Classic Perlin 3D Noise
// by Stefan Gustavson
// https://github.com/visionary-3d/noise-collection/blob/main/glsl/perlin.glsl
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
float noise(vec3 P){
vec3 Pi0 = floor(P); // Integer part for indexing
vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
Pi0 = mod(Pi0, 289.0);
Pi1 = mod(Pi1, 289.0);
vec3 Pf0 = fract(P); // Fractional part for interpolation
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
vec4 iy = vec4(Pi0.yy, Pi1.yy);
vec4 iz0 = Pi0.zzzz;
vec4 iz1 = Pi1.zzzz;
vec4 ixy = permute(permute(ix) + iy);
vec4 ixy0 = permute(ixy + iz0);
vec4 ixy1 = permute(ixy + iz1);
vec4 gx0 = ixy0 / 7.0;
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
gx0 = fract(gx0);
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
vec4 sz0 = step(gz0, vec4(0.0));
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
vec4 gx1 = ixy1 / 7.0;
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
gx1 = fract(gx1);
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
vec4 sz1 = step(gz1, vec4(0.0));
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
g000 *= norm0.x;
g010 *= norm0.y;
g100 *= norm0.z;
g110 *= norm0.w;
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
g001 *= norm1.x;
g011 *= norm1.y;
g101 *= norm1.z;
g111 *= norm1.w;
float n000 = dot(g000, Pf0);
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
float n111 = dot(g111, Pf1);
vec3 fade_xyz = fade(Pf0);
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
return 2.2 * n_xyz;
}
////////////////////////////////////
// lines ///////////////////////////
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
float diffuseFactor(vec3 normal, vec3 light_direction) {
float df = dot(normalize(normal), normalize(light_direction));
if (gl_FrontFacing) df = -df;
return max(0.0, df);
}
float horizontalLine(vec2 pixel, float y_pos, float width) {
return 1.0 - smoothstep(-1.0, 1.0, abs(pixel.y - y_pos) - 0.5 * width);
}
////////////////////////////////////
// main ////////////////////////////
void main() {
float min_resolution = min(u_resolution.x, u_resolution.y);
vec3 light_direction = vec3((u_mouse - 0.3 * u_resolution) / min_resolution, -.99);
float df = diffuseFactor(v_normal, light_direction) * .90;
vec2 pos = gl_FragCoord.xy - 0.5 * u_resolution;
// NOISE: warp coordinate space (main effect)
float warp = noise(vec3(pos * 0.02, 0.4));
pos.y += warp * 8.0;
// rotate
pos = rotate(radians(90.0)) * pos;
// noise for local variation
float n1 = noise(vec3(pos * 0.05, 1.));
float n2 = noise(vec3(pos * 0.1, 0.7));
// line properties
float line_width = 6.0 * (1.0 - smoothstep(0.0, 0.5, df)) + 0.4;
line_width += n2 * 1.2;
float lines_sep = 8.0;
vec2 grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
float line_1 = horizontalLine(
grid_pos,
lines_sep / 8.0 + n1 * 2.0,
line_width
);
grid_pos.y = mod(pos.y + lines_sep / 2.0, lines_sep);
float line_2 = horizontalLine(
grid_pos,
lines_sep / 8.0 + n1 * 2.0,
line_width
);
// second set
pos = rotate(radians(-0.5)) * pos;
lines_sep = 4.0;
grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
float n3 = noise(vec3(pos * 0.03, 1.));
float line_3 = horizontalLine(
grid_pos,
lines_sep / 2.0 + n3 * 2.0,
line_width
);
grid_pos.y = mod(pos.y + lines_sep / 2.0, lines_sep);
float line_4 = horizontalLine(
grid_pos,
lines_sep / 1.0 + n3 * 2.0,
line_width * 0.7
);
// shading
float surface_color = 0.9;
surface_color -= 1.0 * line_1 * (1.0 - smoothstep(0.7, 0.9, df / 1.02));
surface_color -= 0.8 * line_2 * (1.0 - smoothstep(0.35, 0.5, df));
surface_color -= 0.8 * line_3 * (1.0 - smoothstep(0.6, 0.65, df));
surface_color -= 0.8 * line_4 * (1.0 - smoothstep(0.3, 0.4, df));
surface_color = clamp(surface_color, 0.05, 1.0);
gl_FragColor = vec4(vec3(surface_color), 1.0);
}
`
//////////////////////////////////////////////////////////////////////////////////////////////
/// setup ////////////////////////////////////////////////////////////////////////////////////
export const viewport = {
width: 0,
height: 0,
devicePixelRatio: 1,
aspectRatio: 0,
};
export const resizeViewport = () => {
viewport.width = window.innerWidth;
viewport.height = window.innerHeight;
viewport.aspectRatio = viewport.width / viewport.height;
viewport.devicePixelRatio = Math.min(window.devicePixelRatio, 2);
};
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
const scene = new THREE.Scene();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// Camera - fov, aspect, near, far
const camera = new THREE.PerspectiveCamera(
45, //fov
window.innerWidth / window.innerHeight, //aspect
2, //near clip
10000 //far clip
);
// x y z
camera.position.set(0, 0, 10);
//camera.position.y = 50;
//camera.position.z = 20;
// orbit controls //
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
renderer.render(scene, camera);
//////////////////////////////////////////////////////////////////////////////////////////////
/// geometry /////////////////////////////////////////////////////////////////////////////////
const uniforms = {
u_resolution: { value:{ x:0, y:0 }},
}
const material = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: vshader,
fragmentShader: fshader
} );
const objLoader = new OBJLoader();
//const fbxLoader = new FBXLoader();
objLoader.load('Model_All_Low.obj', (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(.07, .07, .07)
scene.add(object);
});
scene.background = new THREE.Color(0xe5e5e5);
//////////////////////////////////////////////////////////////////////////////////////////////
/// Rendering ////////////////////////////////////////////////////////////////////////////////
onWindowResize();
console.log(scene);
animate();
function onWindowResize( event ) {
const aspectRatio = window.innerWidth/window.innerHeight;
let width, height;
if (aspectRatio>=1){
width = 1;
height = (window.innerHeight/window.innerWidth) * width;
}else{
width = aspectRatio;
height = 1;
}
camera.left = -width;
camera.right = width;
camera.top = height;
camera.bottom = -height;
camera.updateProjectionMatrix();
uniforms.u_resolution.value.x = window.innerWidth;
uniforms.u_resolution.value.y = window.innerHeight;
}
window.addEventListener('resize', onWindowResize);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
Next Up: implementing an outline shader to get more detail.
Using a few examples of code,
I mean, if you really want the list:
I was able to get a sobel edge detection filter working, although, not quite to my vision.

After some tinkering, I was able to achieve this result.

But now my issue is that it is in negative!
Here is the sobel filter without the ink shader attached:


Without creating my own shader, in order to edit the values myself, I won’t be able tweak this. I also need to figure out how to invert it 🙁
To try another method, I tried the legendary Inverted Hull method, by creating a clone of the mesh, and scaling it by its normals.
Inverted Hull Code
const uniforms = {
u_resolution: { value:{ x:0, y:0 }},
}
const material = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: vshader,
fragmentShader: fshader,
} );
const outlineMaterial = new THREE.ShaderMaterial({
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normal;
// expand along normal
vec3 newPosition = position + normal * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
`,
fragmentShader: `
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
`,
side: THREE.BackSide
});
const objLoader = new OBJLoader();
objLoader.load('Model_All_Low.obj', (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;
// OUTLINE mesh (duplicate)
const outlineMesh = child.clone();
outlineMesh.material = outlineMaterial;
// add slightly behind
outlineMesh.renderOrder = 0;
child.renderOrder = 1;
outlineMesh.scale.set(.07, .07, .07);
outlineMesh.position.set(0, -10, 0);
scene.add(outlineMesh);
}
});
object.position.set(0, -10, 0)
object.scale.set(.07, .07, .07)
scene.add(object);
});
Overall, its a good look, but it cannot capture the blazer lapels, and becomes too distorted at the face.


Code by this point [12/04/26]
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
//import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
import { SobelOperatorShader } from 'three/addons/shaders/SobelOperatorShader.js';
//////////////////////////////////////////////////////////////////////////////////////////////
/// shaders //////////////////////////////////////////////////////////////////////////////////
// Shader code adapted from --> https://webgl-shaders.com/pencil-example.html
const vshader = `
#define GLSLIFY 1
// Common varyings
varying vec3 v_position;
varying vec3 v_normal;
varying vec2 vUv;
/*
* The main program
*/
void main() {
// Save the varyings
v_position = position;
v_normal = normalize(normalMatrix * normal);
// Vertex shader output
// model matrix -> position, scale, rotation of model
// view matrix -> position, orientation of camera
// projection matrix -> projects object onto screen ( aspect ratio and perspective )
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
const fshader = `
#define GLSLIFY 1
uniform vec2 u_resolution;
uniform vec2 u_mouse;
varying vec3 v_position;
varying vec3 v_normal;
////////////////////////////////////
// noise ///////////////////////////
// Classic Perlin 3D Noise
// by Stefan Gustavson
// https://github.com/visionary-3d/noise-collection/blob/main/glsl/perlin.glsl
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
float noise(vec3 P){
vec3 Pi0 = floor(P); // Integer part for indexing
vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
Pi0 = mod(Pi0, 289.0);
Pi1 = mod(Pi1, 289.0);
vec3 Pf0 = fract(P); // Fractional part for interpolation
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
vec4 iy = vec4(Pi0.yy, Pi1.yy);
vec4 iz0 = Pi0.zzzz;
vec4 iz1 = Pi1.zzzz;
vec4 ixy = permute(permute(ix) + iy);
vec4 ixy0 = permute(ixy + iz0);
vec4 ixy1 = permute(ixy + iz1);
vec4 gx0 = ixy0 / 7.0;
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
gx0 = fract(gx0);
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
vec4 sz0 = step(gz0, vec4(0.0));
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
vec4 gx1 = ixy1 / 7.0;
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
gx1 = fract(gx1);
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
vec4 sz1 = step(gz1, vec4(0.0));
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
g000 *= norm0.x;
g010 *= norm0.y;
g100 *= norm0.z;
g110 *= norm0.w;
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
g001 *= norm1.x;
g011 *= norm1.y;
g101 *= norm1.z;
g111 *= norm1.w;
float n000 = dot(g000, Pf0);
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
float n111 = dot(g111, Pf1);
vec3 fade_xyz = fade(Pf0);
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
return 2.2 * n_xyz;
}
////////////////////////////////////
// lines ///////////////////////////
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
float diffuseFactor(vec3 normal, vec3 light_direction) {
float df = dot(normalize(normal), normalize(light_direction));
if (gl_FrontFacing) df = -df;
return max(0.0, df);
}
float horizontalLine(vec2 pixel, float y_pos, float width) {
return 1.0 - smoothstep(-1.0, 1.0, abs(pixel.y - y_pos) - 0.5 * width);
}
////////////////////////////////////
// main ////////////////////////////
void main() {
float min_resolution = min(u_resolution.x, u_resolution.y);
vec3 light_direction = vec3((u_mouse - 0.3 * u_resolution) / min_resolution, -.99);
float df = diffuseFactor(v_normal, light_direction) * .90;
vec2 pos = gl_FragCoord.xy - 0.5 * u_resolution;
// NOISE: warp coordinate space (main effect)
float warp = noise(vec3(pos * 0.02, 0.4));
pos.y += warp * 8.0;
// rotate
pos = rotate(radians(90.0)) * pos;
// noise for local variation
float n1 = noise(vec3(pos * 0.05, 1.));
float n2 = noise(vec3(pos * 0.1, 0.7));
// line properties
float line_width = 6.0 * (1.0 - smoothstep(0.0, 0.5, df)) + 0.4;
line_width += n2 * 1.2;
float lines_sep = 8.0;
vec2 grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
float line_1 = horizontalLine(
grid_pos,
lines_sep / 8.0 + n1 * 2.0,
line_width
);
grid_pos.y = mod(pos.y + lines_sep / 2.0, lines_sep);
float line_2 = horizontalLine(
grid_pos,
lines_sep / 8.0 + n1 * 2.0,
line_width
);
// second set
pos = rotate(radians(-0.5)) * pos;
lines_sep = 4.0;
grid_pos = vec2(pos.x, mod(pos.y, lines_sep));
float n3 = noise(vec3(pos * 0.03, 1.));
float line_3 = horizontalLine(
grid_pos,
lines_sep / 2.0 + n3 * 2.0,
line_width
);
grid_pos.y = mod(pos.y + lines_sep / 2.0, lines_sep);
float line_4 = horizontalLine(
grid_pos,
lines_sep / 1.0 + n3 * 2.0,
line_width * 0.7
);
// shading
float surface_color = 0.9;
surface_color -= 1.0 * line_1 * (1.0 - smoothstep(0.7, 0.9, df / 1.02));
surface_color -= 0.8 * line_2 * (1.0 - smoothstep(0.35, 0.5, df));
surface_color -= 0.8 * line_3 * (1.0 - smoothstep(0.6, 0.65, df));
surface_color -= 0.8 * line_4 * (1.0 - smoothstep(0.3, 0.4, df));
surface_color = clamp(surface_color, 0.05, 1.0);
gl_FragColor = vec4(vec3(surface_color), 1.0);
}
`
//////////////////////////////////////////////////////////////////////////////////////////////
/// setup ////////////////////////////////////////////////////////////////////////////////////
const renderer = new THREE.WebGLRenderer({canvas:canvas, antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
//renderer.setClearColor('#000000')
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
// Camera - fov, aspect, near, far
const camera = new THREE.PerspectiveCamera(
45, //fov
window.innerWidth / window.innerHeight, //aspect
0.1, //near clip
10000 //far clip
);
// x y z
camera.position.set(0, 0, 10);
//camera.position.y = 50;
//camera.position.z = 20;
// orbit controls //
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
renderer.render(scene, camera);
const color = 0xFFFFFF;
const intensity = 6;
const light = new THREE.DirectionalLight( color, intensity );
light.position.set( - 1, 2, 4 );
scene.add( light );
//////////////////////////////////////////////////////////////////////////////////////////////
/// geometry /////////////////////////////////////////////////////////////////////////////////
const uniforms = {
u_resolution: { value:{ x:0, y:0 }},
}
const material = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: vshader,
fragmentShader: fshader,
} );
//const material = new THREE.MeshPhongMaterial();
//material.color.setHSL(0, 1, .5); // red
const outlineMaterial = new THREE.ShaderMaterial({
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normal;
// expand along normal
vec3 newPosition = position + normal * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
`,
fragmentShader: `
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
`,
side: THREE.BackSide
});
outlineMaterial.side = THREE.BackSide;
const objLoader = new OBJLoader();
//const fbxLoader = new FBXLoader();
objLoader.load('Model_All_Low.obj', (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;
// OUTLINE mesh (duplicate)
const outlineMesh = child.clone();
outlineMesh.material = outlineMaterial;
// add slightly behind
outlineMesh.renderOrder = 0;
child.renderOrder = 1;
outlineMesh.scale.set(.07, .07, .07);
outlineMesh.position.set(0, -10, 0);
scene.add(outlineMesh);
}
});
object.position.set(0, -10, 0)
object.scale.set(.07, .07, .07)
scene.add(object);
});
scene.background = new THREE.Color(0xe5e5e5);
//////////////////////////////////////////////////////////////////////////////////////////////
/// Rendering ////////////////////////////////////////////////////////////////////////////////
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// Sobel operator
const sobelEffect = new ShaderPass(SobelOperatorShader);
sobelEffect.uniforms['resolution'].value.x = (window.innerWidth * window.devicePixelRatio) * 10;
sobelEffect.uniforms['resolution'].value.y = (window.innerHeight * window.devicePixelRatio) * 10;
//composer.addPass(sobelEffect);
//sobelEffect.renderToScreen = true;
// film pass
const filmPass = new FilmPass(
1.5, // intensity
false, // grayscale
);
//composer.addPass( filmPass );
filmPass.renderToScreen = true;
onWindowResize();
console.log(scene);
animate();
function onWindowResize( event ) {
const aspectRatio = window.innerWidth/window.innerHeight;
let width, height;
if (aspectRatio>=1){
width = 1;
height = (window.innerHeight/window.innerWidth) * width;
}else{
width = aspectRatio;
height = 1;
}
camera.left = -width;
camera.right = width;
camera.top = height;
camera.bottom = -height;
camera.updateProjectionMatrix();
composer.setSize( window.innerWidth, window.innerHeight );
uniforms.u_resolution.value.x = window.innerWidth;
uniforms.u_resolution.value.y = window.innerHeight;
}
window.addEventListener('resize', onWindowResize);
function animate() {
requestAnimationFrame(animate);
composer.render();
}
animate();
It does look quite nice on the robot hands however,

By finding the sobel filter and copying it to create my own version, I managed to invert the filter. However, this got me an unwanted output. Let’s see if I can tweak it.

Now I get an outcome that looks like this:

This tells me that the sobel shader is rendering after the ink shader, which needs to be the other way around.
What I can do to fix that is using render passes, and moving my ink shader to a render pass to control when it renders. However that is very complicated, and I would need to research that.

Leave a Reply