├── .babelrc
├── .gitignore
├── .npmignore
├── LICENSE.md
├── README.md
├── app
├── bundle.js
├── index.html
└── main.css
├── lib
├── geom.js
├── gui.json
├── index.js
├── wire.frag
└── wire.vert
├── package-lock.json
├── package.json
└── screenshots
├── banner.jpg
├── edge-removal.png
└── screenshot.png
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: [ "es2015" ]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | *.log
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | *.log
4 | .DS_Store
5 | bundle.js
6 | test
7 | test.js
8 | demo/
9 | .npmignore
10 | LICENSE.md
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2017 Matt DesLauriers
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
20 | OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # webgl-wireframes
2 |
3 | 
4 |
5 | This is the code for a November 2017 net magazine tutorial, _Stylized Wireframe Rendering in WebGL_. Check out the article (when it's released) for more details.
6 |
7 | ## Stylized Wireframe Rendering in WebGL
8 |
9 | The code here uses barycentric coordinates to create stylized wireframes in ThreeJS and WebGL. Some features of the code and its shaders include:
10 |
11 | - Alpha to Coverage for crisp alpha cutouts and depth testing with Multisample Anti-Aliasing
12 | - Thick and anti-aliased single-pass wireframe rendering
13 | - Basic support for animated line dashes
14 | - Inner edge removal to render quads instead of triangles
15 | - A few other effects, such as noise, tapered lines, dual strokes and backface coloring
16 |
17 | ## Demo
18 |
19 | Click [here](https://mattdesl.github.io/webgl-wireframes/app/) to see a live demo.
20 |
21 | [ ](https://mattdesl.github.io/webgl-wireframes/app/)
22 |
23 | ## Usage
24 |
25 | To build & run this project locally, first clone the repository, then use npm to install and run it:
26 |
27 | ```sh
28 | npm install
29 | npm start
30 | ```
31 |
32 | Now open `localhost:9966` to see it in your browser.
33 |
34 | To build:
35 |
36 | ```sh
37 | npm run build
38 | ```
39 |
40 | ## Further Reading
41 |
42 | The technique here is just one approach to wireframe rendering. You may find these other articles interesting:
43 |
44 | - [Easy Wireframes with barycentric coordinates – Florian Bösch](http://codeflow.org/entries/2012/aug/02/easy-wireframe-display-with-barycentric-coordinates/)
45 | - [Two Methods for Antialiased Wireframe Drawing with Hidden Line Removal](http://dl.acm.org/citation.cfm?id=1921300)
46 | - [glsl-solid-wireframe – drawing wireframes and grids in a fragment shader by Ricky Reusser](https://github.com/rreusser/glsl-solid-wireframe)
47 | - [Drawing Lines is Hard](https://mattdesl.svbtle.com/drawing-lines-is-hard)
48 |
49 | ## License
50 |
51 | MIT, see [LICENSE.md](http://github.com/mattdesl/webgl-wireframes/blob/master/LICENSE.md) for details.
52 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | webgl-wireframes
7 |
8 |
9 |
10 |
11 |
12 |
13 | Stylized Wireframe Rendering in WebGL
14 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Raleway', 'Helvetica', sans-serif;
3 | font-size: 11px;
4 | background: #fff;
5 | color: #222222;
6 | overflow: hidden;
7 | margin: 0;
8 | padding: 0;
9 | -webkit-text-size-adjust: 100%;
10 | }
11 |
12 | html {
13 | width: 100%;
14 | height: 100%;
15 | font-size: 62.5%;
16 | }
17 |
18 | * {
19 | -webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */
20 | -webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */
21 | -webkit-tap-highlight-color: rgba(0,0,0,0); /* prevent tap highlight color / shadow */
22 | -webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */
23 | }
24 |
25 | canvas {
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | }
30 |
31 | .info-container {
32 | position: absolute;
33 | bottom: 0;
34 | left: 0;
35 | padding: 1.5rem;
36 | }
37 |
38 | .main-title {
39 | position: absolute;
40 | top: 0;
41 | left: 0;
42 | padding: 1.5rem;
43 | font-size: 12px;
44 | }
45 |
46 | .info {
47 | font-weight: 700;
48 | }
49 |
50 | .code {
51 | font-weight: 300;
52 | }
53 |
54 | p {
55 | line-height: 0.6rem;
56 | }
57 |
58 | a, a:active, a:visited {
59 | color: #2b84e0;
60 | text-decoration: none;
61 | position: relative;
62 | }
63 | a::after {
64 | position: absolute;
65 | content: ' ';
66 | display: block;
67 | width: 100%;
68 | height: 2px;
69 | bottom: 0px;
70 | opacity: 0;
71 | left: 0;
72 | background: currentColor;
73 | -webkit-transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
74 | -moz-transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
75 | -o-transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
76 | transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
77 | }
78 | a:hover::after {
79 | height: 2px;
80 | opacity: 1;
81 | bottom: -3px;
82 | -webkit-transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
83 | -moz-transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
84 | -o-transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
85 | transition: all 0.25s cubic-bezier(0.190, 1.000, 0.220, 1.000);
86 | }
--------------------------------------------------------------------------------
/lib/geom.js:
--------------------------------------------------------------------------------
1 | module.exports.addBarycentricCoordinates = function addBarycentricCoordinates (bufferGeometry, removeEdge = false) {
2 | const attrib = bufferGeometry.getIndex() || bufferGeometry.getAttribute('position');
3 | const count = attrib.count / 3;
4 | const barycentric = [];
5 |
6 | // for each triangle in the geometry, add the barycentric coordinates
7 | for (let i = 0; i < count; i++) {
8 | const even = i % 2 === 0;
9 | const Q = removeEdge ? 1 : 0;
10 | if (even) {
11 | barycentric.push(
12 | 0, 0, 1,
13 | 0, 1, 0,
14 | 1, 0, Q
15 | );
16 | } else {
17 | barycentric.push(
18 | 0, 1, 0,
19 | 0, 0, 1,
20 | 1, 0, Q
21 | );
22 | }
23 | }
24 |
25 | // add the attribute to the geometry
26 | const array = new Float32Array(barycentric);
27 | const attribute = new THREE.BufferAttribute(array, 3);
28 | bufferGeometry.addAttribute('barycentric', attribute);
29 | };
30 |
31 | module.exports.unindexBufferGeometry = function unindexBufferGeometry (bufferGeometry) {
32 | // un-indices the geometry, copying all attributes like position and uv
33 | const index = bufferGeometry.getIndex();
34 | if (!index) return; // already un-indexed
35 |
36 | const indexArray = index.array;
37 | const triangleCount = indexArray.length / 3;
38 |
39 | const attributes = bufferGeometry.attributes;
40 | const newAttribData = Object.keys(attributes).map(key => {
41 | return {
42 | array: [],
43 | attribute: bufferGeometry.getAttribute(key)
44 | };
45 | });
46 |
47 | for (let i = 0; i < triangleCount; i++) {
48 | // indices into attributes
49 | const a = indexArray[i * 3 + 0];
50 | const b = indexArray[i * 3 + 1];
51 | const c = indexArray[i * 3 + 2];
52 | const indices = [ a, b, c ];
53 |
54 | // for each attribute, put vertex into unindexed list
55 | newAttribData.forEach(data => {
56 | const attrib = data.attribute;
57 | const dim = attrib.itemSize;
58 | // add [a, b, c] vertices
59 | for (let i = 0; i < indices.length; i++) {
60 | const index = indices[i];
61 | for (let d = 0; d < dim; d++) {
62 | const v = attrib.array[index * dim + d];
63 | data.array.push(v);
64 | }
65 | }
66 | });
67 | }
68 | index.array = null;
69 | bufferGeometry.setIndex(null);
70 |
71 | // now copy over new data
72 | newAttribData.forEach(data => {
73 | const newArray = new data.attribute.array.constructor(data.array);
74 | data.attribute.setArray(newArray);
75 | });
76 | };
77 |
--------------------------------------------------------------------------------
/lib/gui.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "Sleek",
3 | "closed": false,
4 | "remembered": {
5 | "Sleek": {
6 | "0": {
7 | "seeThrough": true,
8 | "thickness": 0.01,
9 | "backgroundHex": "#e3e3e3",
10 | "fillHex": "#c5c5c5",
11 | "strokeHex": "#202020",
12 | "dashEnabled": true,
13 | "dashAnimate": false,
14 | "dashRepeats": 9,
15 | "dashLength": 0.7000000000000001,
16 | "dashOverlap": true,
17 | "noiseA": false,
18 | "noiseB": false,
19 | "insideAltColor": true,
20 | "squeeze": true,
21 | "squeezeMin": 0.1,
22 | "squeezeMax": 1,
23 | "dualStroke": false,
24 | "secondThickness": 0.05,
25 | "name": "Icosphere",
26 | "edgeRemoval": false
27 | }
28 | },
29 | "FunkyTorus": {
30 | "0": {
31 | "seeThrough": false,
32 | "thickness": 0.02,
33 | "backgroundHex": "#e8d5b7",
34 | "fillHex": "#0e2430",
35 | "strokeHex": "#fc3a51",
36 | "dashEnabled": false,
37 | "dashAnimate": false,
38 | "dashRepeats": 1,
39 | "dashLength": 0.8,
40 | "dashOverlap": true,
41 | "noiseA": false,
42 | "noiseB": false,
43 | "insideAltColor": true,
44 | "squeeze": true,
45 | "squeezeMin": 0,
46 | "squeezeMax": 1,
47 | "dualStroke": true,
48 | "secondThickness": 0.05,
49 | "name": "TorusKnot",
50 | "edgeRemoval": true
51 | }
52 | },
53 | "BlockyFade": {
54 | "0": {
55 | "seeThrough": true,
56 | "thickness": 0.01,
57 | "backgroundHex": "#eff3cd",
58 | "fillHex": "#b2d5ba",
59 | "strokeHex": "#61ada0",
60 | "dashEnabled": false,
61 | "dashAnimate": false,
62 | "dashRepeats": 1,
63 | "dashLength": 0.9,
64 | "dashOverlap": true,
65 | "noiseA": true,
66 | "noiseB": false,
67 | "insideAltColor": true,
68 | "squeeze": true,
69 | "squeezeMin": 0.1,
70 | "squeezeMax": 1,
71 | "dualStroke": false,
72 | "secondThickness": 0.05,
73 | "name": "Tube",
74 | "edgeRemoval": true
75 | }
76 | },
77 | "SimpleWire": {
78 | "0": {
79 | "seeThrough": true,
80 | "thickness": 0.03,
81 | "backgroundHex": "#f2f2f2",
82 | "fillHex": "#d2d2d2",
83 | "strokeHex": "#000000",
84 | "dashEnabled": false,
85 | "dashAnimate": false,
86 | "dashRepeats": 1,
87 | "dashLength": 0.8,
88 | "dashOverlap": true,
89 | "noiseA": false,
90 | "noiseB": false,
91 | "insideAltColor": false,
92 | "squeeze": false,
93 | "squeezeMin": 0.5,
94 | "squeezeMax": 1,
95 | "dualStroke": false,
96 | "secondThickness": 0.05,
97 | "name": "Icosphere",
98 | "edgeRemoval": false
99 | }
100 | },
101 | "NicerWire": {
102 | "0": {
103 | "seeThrough": true,
104 | "thickness": 0.03,
105 | "backgroundHex": "#f6f6f6",
106 | "fillHex": "#e6e6e6",
107 | "strokeHex": "#252525",
108 | "dashEnabled": false,
109 | "dashAnimate": false,
110 | "dashRepeats": 1,
111 | "dashLength": 0.8,
112 | "dashOverlap": true,
113 | "noiseA": false,
114 | "noiseB": false,
115 | "insideAltColor": true,
116 | "squeeze": true,
117 | "squeezeMin": 0.4,
118 | "squeezeMax": 1,
119 | "dualStroke": false,
120 | "secondThickness": 0.05,
121 | "name": "Torus",
122 | "edgeRemoval": true
123 | }
124 | },
125 | "Animated": {
126 | "0": {
127 | "seeThrough": false,
128 | "thickness": 0.02,
129 | "backgroundHex": "#fdf1cc",
130 | "fillHex": "#c6d6b8",
131 | "strokeHex": "#987f69",
132 | "dashEnabled": true,
133 | "dashAnimate": true,
134 | "dashRepeats": 6,
135 | "dashLength": 0.6000000000000001,
136 | "dashOverlap": true,
137 | "noiseA": false,
138 | "noiseB": false,
139 | "insideAltColor": false,
140 | "squeeze": true,
141 | "squeezeMin": 0.2,
142 | "squeezeMax": 1,
143 | "dualStroke": false,
144 | "secondThickness": 0.03,
145 | "name": "TorusKnot",
146 | "edgeRemoval": true
147 | }
148 | },
149 | "FunZone": {
150 | "0": {
151 | "seeThrough": true,
152 | "thickness": 0.02,
153 | "backgroundHex": "#dad6ca",
154 | "fillHex": "#1bb0ce",
155 | "strokeHex": "#4f8699",
156 | "dashEnabled": false,
157 | "dashAnimate": false,
158 | "dashRepeats": 2,
159 | "dashLength": 0.7000000000000001,
160 | "dashOverlap": true,
161 | "noiseA": true,
162 | "noiseB": true,
163 | "insideAltColor": true,
164 | "squeeze": true,
165 | "squeezeMin": 0.2,
166 | "squeezeMax": 1,
167 | "dualStroke": false,
168 | "secondThickness": 0.03,
169 | "name": "Icosphere",
170 | "edgeRemoval": false
171 | }
172 | },
173 | "Dotted": {
174 | "0": {
175 | "seeThrough": true,
176 | "thickness": 0.04,
177 | "backgroundHex": "#e69472",
178 | "fillHex": "#cf7f5e",
179 | "strokeHex": "#933333",
180 | "dashEnabled": true,
181 | "dashAnimate": false,
182 | "dashRepeats": 8,
183 | "dashLength": 0.5,
184 | "dashOverlap": true,
185 | "noiseA": false,
186 | "noiseB": false,
187 | "insideAltColor": true,
188 | "squeeze": true,
189 | "squeezeMin": 0.2,
190 | "squeezeMax": 1,
191 | "dualStroke": false,
192 | "secondThickness": 0.05,
193 | "name": "Sphere",
194 | "edgeRemoval": true
195 | }
196 | },
197 | "Flower": {
198 | "0": {
199 | "seeThrough": true,
200 | "thickness": 0.09,
201 | "backgroundHex": "#edebe6",
202 | "fillHex": "#d6e1c7",
203 | "strokeHex": "#94c7b6",
204 | "dashEnabled": true,
205 | "dashAnimate": false,
206 | "dashRepeats": 1,
207 | "dashLength": 0.8,
208 | "dashOverlap": false,
209 | "noiseA": false,
210 | "noiseB": false,
211 | "insideAltColor": true,
212 | "squeeze": true,
213 | "squeezeMin": 0.2,
214 | "squeezeMax": 0,
215 | "dualStroke": false,
216 | "secondThickness": 0.05,
217 | "name": "Icosphere",
218 | "edgeRemoval": false
219 | }
220 | },
221 | "Tapered": {
222 | "0": {
223 | "seeThrough": false,
224 | "thickness": 0.015,
225 | "backgroundHex": "#a7c5bd",
226 | "fillHex": "#e5ddcb",
227 | "strokeHex": "#eb7b59",
228 | "dashEnabled": true,
229 | "dashAnimate": false,
230 | "dashRepeats": 1,
231 | "dashLength": 0.6,
232 | "dashOverlap": true,
233 | "noiseA": false,
234 | "noiseB": false,
235 | "insideAltColor": true,
236 | "squeeze": true,
237 | "squeezeMin": 0,
238 | "squeezeMax": 0.64,
239 | "dualStroke": false,
240 | "secondThickness": 0.03,
241 | "name": "TorusKnot",
242 | "edgeRemoval": true
243 | }
244 | }
245 | },
246 | "folders": {
247 | "Shader": {
248 | "preset": "Default",
249 | "closed": false,
250 | "folders": {
251 | "Dash": {
252 | "preset": "Default",
253 | "closed": true,
254 | "folders": {}
255 | },
256 | "Effects": {
257 | "preset": "Default",
258 | "closed": true,
259 | "folders": {}
260 | },
261 | "Geometry": {
262 | "preset": "Default",
263 | "closed": false,
264 | "folders": {}
265 | }
266 | }
267 | }
268 | }
269 | }
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | // Require ThreeJS and any necessary extensions
2 | global.THREE = require('three');
3 | require('three/examples/js/curves/NURBSUtils');
4 | require('three/examples/js/curves/NURBSCurve');
5 |
6 | // Include dat.gui sliders
7 | const dat = require('dat.gui/build/dat.gui.js');
8 | const gui = new dat.GUI({ load: require('./gui.json'), preset: 'Sleek' });
9 |
10 | // Grab some nice color palettes
11 | const palettes = require('nice-color-palettes');
12 |
13 | // glslify is used for including GLSL shader code
14 | const glslify = require('glslify');
15 | const path = require('path');
16 |
17 | // our geometry helper functions
18 | const {
19 | addBarycentricCoordinates,
20 | unindexBufferGeometry
21 | } = require('./geom');
22 |
23 | // grab a default palette
24 | let palette = palettes[13].slice();
25 |
26 | const canvas = document.querySelector('#canvas');
27 |
28 | // the 1st color in our palette is our background
29 | const background = palette.shift();
30 | canvas.style.background = background;
31 |
32 | // create an anti-aliased ThreeJS WebGL renderer
33 | const renderer = new THREE.WebGLRenderer({
34 | antialias: true,
35 | canvas
36 | });
37 |
38 | // Enable Alpha to Coverage for alpha cutouts + depth test
39 | const gl = renderer.getContext();
40 | gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
41 |
42 | // Configure renderer
43 | renderer.setClearColor(background, 1);
44 | renderer.setPixelRatio(window.devicePixelRatio);
45 |
46 | // Listen for window resizes
47 | window.addEventListener('resize', () => resize());
48 |
49 | // Create a scene and camera for 3D world
50 | const scene = new THREE.Scene();
51 | const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 100);
52 |
53 | // Create our custom wireframe shader material
54 | const material = new THREE.ShaderMaterial({
55 | extensions: {
56 | // needed for anti-alias smoothstep, aastep()
57 | derivatives: true
58 | },
59 | transparent: true,
60 | side: THREE.DoubleSide,
61 | uniforms: { // some parameters for the shader
62 | time: { value: 0 },
63 | fill: { value: new THREE.Color(palette[0]) },
64 | stroke: { value: new THREE.Color(palette[1]) },
65 | noiseA: { value: false },
66 | noiseB: { value: false },
67 | dualStroke: { value: false },
68 | seeThrough: { value: false },
69 | insideAltColor: { value: true },
70 | thickness: { value: 0.01 },
71 | secondThickness: { value: 0.05 },
72 | dashEnabled: { value: true },
73 | dashRepeats: { value: 2.0 },
74 | dashOverlap: { value: false },
75 | dashLength: { value: 0.55 },
76 | dashAnimate: { value: false },
77 | squeeze: { value: false },
78 | squeezeMin: { value: 0.1 },
79 | squeezeMax: { value: 1.0 }
80 | },
81 | // use glslify here to bring in the GLSL code
82 | fragmentShader: glslify(path.resolve(__dirname, 'wire.frag')),
83 | vertexShader: glslify(path.resolve(__dirname, 'wire.vert'))
84 | });
85 |
86 | // add the mesh with an empty geometry for now, we will change it later
87 | const mesh = new THREE.Mesh(new THREE.Geometry(), material);
88 | scene.add(mesh);
89 |
90 | const clock = new THREE.Clock();
91 |
92 | // set up scene and start drawing
93 | createGeometry();
94 | setupGUI();
95 | resize();
96 | renderer.animate(update);
97 | canvas.style.visibility = '';
98 | update();
99 | draw();
100 |
101 | function update () {
102 | // orbit the camera and update shader time
103 | const time = clock.getElapsedTime();
104 | const radius = 4;
105 | const angle = time * 2.5 * Math.PI / 180;
106 | camera.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
107 | camera.lookAt(new THREE.Vector3());
108 | mesh.material.uniforms.time.value = time;
109 | draw();
110 | }
111 |
112 | function draw () {
113 | // render a single frame
114 | renderer.render(scene, camera);
115 | }
116 |
117 | function resize (width = window.innerWidth, height = window.innerHeight, pixelRatio = window.devicePixelRatio) {
118 | // handle window resize
119 | renderer.setPixelRatio(pixelRatio);
120 | renderer.setSize(width, height);
121 | camera.aspect = width / height;
122 | camera.updateProjectionMatrix();
123 | draw();
124 | }
125 |
126 | function createGeometry (type = 'TorusKnot', edgeRemoval = true) {
127 | // dispose the old geometry if we have one
128 | if (mesh.geometry) mesh.geometry.dispose();
129 |
130 | // here we change the geometry of the mesh to visualize
131 | // the shader in different applications
132 | let geometry;
133 | switch (type) {
134 | case 'TorusKnot':
135 | geometry = new THREE.TorusKnotBufferGeometry(0.7, 0.3, 30, 4);
136 | geometry.rotateY(-Math.PI * 0.5);
137 | break;
138 | case 'Icosphere':
139 | geometry = new THREE.IcosahedronBufferGeometry(1, 1);
140 | break;
141 | case 'Tube':
142 | const baseGeom = new THREE.IcosahedronGeometry(1, 0);
143 | const points = baseGeom.vertices;
144 | baseGeom.dispose();
145 | const curve = toSpline(points);
146 | geometry = new THREE.TubeBufferGeometry(curve, 30, 0.3, 4, false);
147 | break;
148 | case 'Sphere':
149 | geometry = new THREE.SphereBufferGeometry(1, 20, 10);
150 | break;
151 | case 'Torus':
152 | geometry = new THREE.TorusBufferGeometry(1, 0.3, 8, 30);
153 | break;
154 | }
155 |
156 | // the BufferGeometry needs to be un-indexed, then we can
157 | // add barycentric coordiantes for the wireframe effect
158 | unindexBufferGeometry(geometry);
159 | addBarycentricCoordinates(geometry, edgeRemoval);
160 |
161 | // set the new geometry on the mesh
162 | mesh.geometry = geometry;
163 | }
164 |
165 | function toSpline (points) {
166 | // This helper function makes a smooth NURBS curve from a set of points
167 | const nurbsDegree = 3;
168 | const nurbsKnots = [];
169 | for (let i = 0; i <= nurbsDegree; i++) {
170 | nurbsKnots.push(0);
171 | }
172 | let nurbsControlPoints = points.map((p, i, list) => {
173 | const knot = (i + 1) / (list.length - nurbsDegree);
174 | nurbsKnots.push(Math.max(Math.min(1, knot), 0));
175 | return new THREE.Vector4(p.x, p.y, p.z, 1);
176 | });
177 | return new THREE.NURBSCurve(nurbsDegree, nurbsKnots, nurbsControlPoints);
178 | }
179 |
180 | function saveScreenshot () {
181 | // force a specific output size
182 | const width = 2048;
183 | const height = 2048;
184 | resize(width, height, 1);
185 |
186 | const dataURI = canvas.toDataURL('image/png');
187 |
188 | // revert to old size
189 | resize();
190 |
191 | // force download
192 | const link = document.createElement('a');
193 | link.download = 'Screenshot.png';
194 | link.href = dataURI;
195 | link.click();
196 | }
197 |
198 | function setupGUI () {
199 | // This function handles all the GUI sliders and updates
200 | const shader = gui.addFolder('Shader');
201 |
202 | const guiData = {
203 | name: 'TorusKnot',
204 | edgeRemoval: true,
205 | backgroundHex: background,
206 | saveScreenshot,
207 | fillHex: `#${mesh.material.uniforms.fill.value.getHexString()}`,
208 | strokeHex: `#${mesh.material.uniforms.stroke.value.getHexString()}`
209 | };
210 |
211 | // add all the uniforms into our gui data
212 | Object.keys(mesh.material.uniforms).forEach(key => {
213 | const uniform = mesh.material.uniforms[key];
214 | if (typeof uniform.value === 'boolean' || typeof uniform.value === 'number') {
215 | guiData[key] = uniform.value;
216 | }
217 | });
218 |
219 | const randomColors = () => {
220 | palette = palettes[Math.floor(Math.random() * palettes.length)].slice();
221 | guiData.backgroundHex = palette.shift();
222 | guiData.fillHex = palette[0];
223 | guiData.strokeHex = palette[1];
224 | updateColors();
225 |
226 | // Iterate over all controllers
227 | for (let k in gui.__folders.Shader.__controllers) {
228 | gui.__folders.Shader.__controllers[k].updateDisplay();
229 | }
230 | };
231 |
232 | const updateColors = () => {
233 | canvas.style.background = guiData.backgroundHex;
234 | renderer.setClearColor(guiData.backgroundHex, 1.0);
235 | mesh.material.uniforms.fill.value.setStyle(guiData.fillHex);
236 | mesh.material.uniforms.stroke.value.setStyle(guiData.strokeHex);
237 | };
238 |
239 | const updateUniforms = () => {
240 | Object.keys(guiData).forEach(key => {
241 | if (key in mesh.material.uniforms) {
242 | mesh.material.uniforms[key].value = guiData[key];
243 | }
244 | });
245 | };
246 |
247 | const updateGeom = () => createGeometry(guiData.name, guiData.edgeRemoval);
248 |
249 | guiData.randomColors = randomColors;
250 | gui.remember(guiData);
251 |
252 | shader.add(guiData, 'seeThrough').name('See Through').onChange(updateUniforms);
253 | shader.add(guiData, 'thickness', 0.005, 0.2).step(0.001).name('Thickness').onChange(updateUniforms);
254 | shader.addColor(guiData, 'backgroundHex').name('Background').onChange(updateColors);
255 | shader.addColor(guiData, 'fillHex').name('Fill').onChange(updateColors);
256 | shader.addColor(guiData, 'strokeHex').name('Stroke').onChange(updateColors);
257 | shader.add(guiData, 'randomColors').name('Random Palette');
258 | shader.add(guiData, 'saveScreenshot').name('Save PNG');
259 |
260 | const dash = shader.addFolder('Dash');
261 | dash.add(guiData, 'dashEnabled').name('Enabled').onChange(updateUniforms);
262 | dash.add(guiData, 'dashAnimate').name('Animate').onChange(updateUniforms);
263 | dash.add(guiData, 'dashRepeats', 1, 10).step(1).name('Repeats').onChange(updateUniforms);
264 | dash.add(guiData, 'dashLength', 0, 1).step(0.01).name('Length').onChange(updateUniforms);
265 | dash.add(guiData, 'dashOverlap').name('Overlap Join').onChange(updateUniforms);
266 |
267 | const effects = shader.addFolder('Effects');
268 | effects.add(guiData, 'noiseA').name('Noise Big').onChange(updateUniforms);
269 | effects.add(guiData, 'noiseB').name('Noise Small').onChange(updateUniforms);
270 | effects.add(guiData, 'insideAltColor').name('Backface Color').onChange(updateUniforms);
271 | effects.add(guiData, 'squeeze').name('Squeeze').onChange(updateUniforms);
272 | effects.add(guiData, 'squeezeMin', 0, 1).step(0.01).name('Squeeze Min').onChange(updateUniforms);
273 | effects.add(guiData, 'squeezeMax', 0, 1).step(0.01).name('Squeeze Max').onChange(updateUniforms);
274 | effects.add(guiData, 'dualStroke').name('Dual Stroke').onChange(updateUniforms);
275 | effects.add(guiData, 'secondThickness', 0, 0.2).step(0.001).name('Dual Thick').onChange(updateUniforms);
276 |
277 | const geom = shader.addFolder('Geometry');
278 | geom.add(guiData, 'name', [
279 | 'TorusKnot',
280 | 'Icosphere',
281 | 'Tube',
282 | 'Sphere',
283 | 'Torus'
284 | ]).name('Geometry').onChange(updateGeom);
285 | geom.add(guiData, 'edgeRemoval').name('Edge Removal').onChange(updateGeom);
286 |
287 | // close GUI for mobile devices
288 | const isMobile = /(Android|iPhone|iOS|iPod|iPad)/i.test(navigator.userAgent);
289 | if (isMobile) {
290 | gui.close();
291 | }
292 |
293 | updateGeom();
294 | updateColors();
295 | updateUniforms();
296 | }
297 |
--------------------------------------------------------------------------------
/lib/wire.frag:
--------------------------------------------------------------------------------
1 | varying vec3 vBarycentric;
2 | varying float vEven;
3 | varying vec2 vUv;
4 | varying vec3 vPosition;
5 |
6 | uniform float time;
7 | uniform float thickness;
8 | uniform float secondThickness;
9 |
10 | uniform float dashRepeats;
11 | uniform float dashLength;
12 | uniform bool dashOverlap;
13 | uniform bool dashEnabled;
14 | uniform bool dashAnimate;
15 |
16 | uniform bool seeThrough;
17 | uniform bool insideAltColor;
18 | uniform bool dualStroke;
19 | uniform bool noiseA;
20 | uniform bool noiseB;
21 |
22 | uniform bool squeeze;
23 | uniform float squeezeMin;
24 | uniform float squeezeMax;
25 |
26 | uniform vec3 stroke;
27 | uniform vec3 fill;
28 |
29 | #pragma glslify: noise = require('glsl-noise/simplex/4d');
30 | #pragma glslify: PI = require('glsl-pi');
31 |
32 | // This is like
33 | float aastep (float threshold, float dist) {
34 | float afwidth = fwidth(dist) * 0.5;
35 | return smoothstep(threshold - afwidth, threshold + afwidth, dist);
36 | }
37 |
38 | // This function is not currently used, but it can be useful
39 | // to achieve a fixed width wireframe regardless of z-depth
40 | float computeScreenSpaceWireframe (vec3 barycentric, float lineWidth) {
41 | vec3 dist = fwidth(barycentric);
42 | vec3 smoothed = smoothstep(dist * ((lineWidth * 0.5) - 0.5), dist * ((lineWidth * 0.5) + 0.5), barycentric);
43 | return 1.0 - min(min(smoothed.x, smoothed.y), smoothed.z);
44 | }
45 |
46 | // This function returns the fragment color for our styled wireframe effect
47 | // based on the barycentric coordinates for this fragment
48 | vec4 getStyledWireframe (vec3 barycentric) {
49 | // this will be our signed distance for the wireframe edge
50 | float d = min(min(barycentric.x, barycentric.y), barycentric.z);
51 |
52 | // we can modify the distance field to create interesting effects & masking
53 | float noiseOff = 0.0;
54 | if (noiseA) noiseOff += noise(vec4(vPosition.xyz * 1.0, time * 0.35)) * 0.15;
55 | if (noiseB) noiseOff += noise(vec4(vPosition.xyz * 80.0, time * 0.5)) * 0.12;
56 | d += noiseOff;
57 |
58 | // for dashed rendering, we can use this to get the 0 .. 1 value of the line length
59 | float positionAlong = max(barycentric.x, barycentric.y);
60 | if (barycentric.y < barycentric.x && barycentric.y < barycentric.z) {
61 | positionAlong = 1.0 - positionAlong;
62 | }
63 |
64 | // the thickness of the stroke
65 | float computedThickness = thickness;
66 |
67 | // if we want to shrink the thickness toward the center of the line segment
68 | if (squeeze) {
69 | computedThickness *= mix(squeezeMin, squeezeMax, (1.0 - sin(positionAlong * PI)));
70 | }
71 |
72 | // if we should create a dash pattern
73 | if (dashEnabled) {
74 | // here we offset the stroke position depending on whether it
75 | // should overlap or not
76 | float offset = 1.0 / dashRepeats * dashLength / 2.0;
77 | if (!dashOverlap) {
78 | offset += 1.0 / dashRepeats / 2.0;
79 | }
80 |
81 | // if we should animate the dash or not
82 | if (dashAnimate) {
83 | offset += time * 0.22;
84 | }
85 |
86 | // create the repeating dash pattern
87 | float pattern = fract((positionAlong + offset) * dashRepeats);
88 | computedThickness *= 1.0 - aastep(dashLength, pattern);
89 | }
90 |
91 | // compute the anti-aliased stroke edge
92 | float edge = 1.0 - aastep(computedThickness, d);
93 |
94 | // now compute the final color of the mesh
95 | vec4 outColor = vec4(0.0);
96 | if (seeThrough) {
97 | outColor = vec4(stroke, edge);
98 | if (insideAltColor && !gl_FrontFacing) {
99 | outColor.rgb = fill;
100 | }
101 | } else {
102 | vec3 mainStroke = mix(fill, stroke, edge);
103 | outColor.a = 1.0;
104 | if (dualStroke) {
105 | float inner = 1.0 - aastep(secondThickness, d);
106 | vec3 wireColor = mix(fill, stroke, abs(inner - edge));
107 | outColor.rgb = wireColor;
108 | } else {
109 | outColor.rgb = mainStroke;
110 | }
111 | }
112 |
113 | return outColor;
114 | }
115 |
116 | void main () {
117 | gl_FragColor = getStyledWireframe(vBarycentric);
118 | }
--------------------------------------------------------------------------------
/lib/wire.vert:
--------------------------------------------------------------------------------
1 | attribute vec3 barycentric;
2 | attribute float even;
3 |
4 | varying vec3 vBarycentric;
5 |
6 | varying vec3 vPosition;
7 | varying float vEven;
8 | varying vec2 vUv;
9 |
10 |
11 | void main () {
12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position.xyz, 1.0);
13 | vBarycentric = barycentric;
14 | vPosition = position.xyz;
15 | vEven = even;
16 | vUv = uv;
17 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webgl-wireframes",
3 | "version": "1.0.0",
4 | "description": "Stylistic Wireframe Rendering in WebGL",
5 | "main": "index.js",
6 | "license": "MIT",
7 | "author": {
8 | "name": "Matt DesLauriers",
9 | "email": "dave.des@gmail.com",
10 | "url": "https://github.com/mattdesl"
11 | },
12 | "dependencies": {
13 | "dat.gui": "github:dataarts/dat.gui",
14 | "glsl-noise": "0.0.0",
15 | "glsl-pi": "^1.0.0",
16 | "glslify": "^6.1.0",
17 | "nice-color-palettes": "^2.0.0",
18 | "three": "^0.87.1"
19 | },
20 | "devDependencies": {
21 | "babel-preset-es2015": "^6.24.1",
22 | "babelify": "^7.3.0",
23 | "browserify": "^14.4.0",
24 | "budo": "^10.0.4",
25 | "uglify-js": "^3.0.28"
26 | },
27 | "scripts": {
28 | "start": "budo lib/index.js:bundle.js --live --dir app -- -t babelify -t glslify",
29 | "build": "browserify lib/index.js -t babelify -t glslify | uglifyjs -m -c warnings=false > app/bundle.js"
30 | },
31 | "keywords": [],
32 | "repository": {
33 | "type": "git",
34 | "url": "git://github.com/mattdesl/webgl-wireframes.git"
35 | },
36 | "semistandard": {
37 | "globals": [
38 | "THREE"
39 | ]
40 | },
41 | "homepage": "https://github.com/mattdesl/webgl-wireframes",
42 | "bugs": {
43 | "url": "https://github.com/mattdesl/webgl-wireframes/issues"
44 | }
45 | }
--------------------------------------------------------------------------------
/screenshots/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/webgl-wireframes/bf073f8f18215dec87cb910f6da3775f1e8bfb6f/screenshots/banner.jpg
--------------------------------------------------------------------------------
/screenshots/edge-removal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/webgl-wireframes/bf073f8f18215dec87cb910f6da3775f1e8bfb6f/screenshots/edge-removal.png
--------------------------------------------------------------------------------
/screenshots/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/webgl-wireframes/bf073f8f18215dec87cb910f6da3775f1e8bfb6f/screenshots/screenshot.png
--------------------------------------------------------------------------------