├── LICENSE
├── README.md
├── examples
├── _js
│ └── root.js
├── animated
│ ├── index.html
│ └── main.js
├── character
│ ├── index.html
│ └── main.js
└── text
│ ├── index.html
│ └── main.js
├── package-lock.json
├── package.json
└── src
└── FuzzyMesh.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Szenia Zadvornykh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # three-fuzzy-mesh
2 |
3 | Code for a Three.js experiment that uses Three.bas to create a fuzzy/hairy mesh based on a Three.js geometry.
4 |
5 | Check out the [Medium post](https://medium.com/@Zadvorsky/fuzzy-meshes-4c7fd3910d6f) for details about the implementation and approach.
6 |
7 | See it running [here](https://codepen.io/dpdknl/pen/JrgrJN/).
8 |
9 | ## Usage
10 |
11 | No package / build system yet. Just grab `src/FuzzyMesh.js` and drop it in your project somewhere.
12 |
--------------------------------------------------------------------------------
/examples/_js/root.js:
--------------------------------------------------------------------------------
1 | function THREERoot(params) {
2 | // defaults
3 | params = Object.assign({
4 | container:'#three-container',
5 | fov:60,
6 | zNear:1,
7 | zFar:10000,
8 | createCameraControls: true,
9 | autoStart: true,
10 | pixelRatio: window.devicePixelRatio,
11 | antialias: (window.devicePixelRatio === 1),
12 | alpha: false
13 | }, params);
14 |
15 | // maps and arrays
16 | this.updateCallbacks = [];
17 | this.resizeCallbacks = [];
18 | this.objects = {};
19 |
20 | // renderer
21 | this.renderer = new THREE.WebGLRenderer({
22 | antialias: params.antialias,
23 | alpha: params.alpha
24 | });
25 | this.renderer.setPixelRatio(params.pixelRatio);
26 |
27 | // container
28 | this.container = (typeof params.container === 'string') ? document.querySelector(params.container) : params.container;
29 | this.container.appendChild(this.renderer.domElement);
30 |
31 | // camera
32 | this.camera = new THREE.PerspectiveCamera(
33 | params.fov,
34 | window.innerWidth / window.innerHeight,
35 | params.zNear,
36 | params.zFar
37 | );
38 |
39 | // scene
40 | this.scene = new THREE.Scene();
41 |
42 | // resize handling
43 | this.resize = this.resize.bind(this);
44 | this.resize();
45 | window.addEventListener('resize', this.resize, false);
46 |
47 | // tick / update / render
48 | this.tick = this.tick.bind(this);
49 | params.autoStart && this.tick();
50 |
51 | // optional camera controls
52 | params.createCameraControls && this.createOrbitControls();
53 | }
54 | THREERoot.prototype = {
55 | createOrbitControls: function() {
56 | this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
57 | this.addUpdateCallback(this.controls.update.bind(this.controls));
58 | },
59 | start: function() {
60 | this.tick();
61 | },
62 | addUpdateCallback: function(callback) {
63 | this.updateCallbacks.push(callback);
64 | },
65 | addResizeCallback: function(callback) {
66 | this.resizeCallbacks.push(callback);
67 | },
68 | add: function(object, key) {
69 | key && (this.objects[key] = object);
70 | this.scene.add(object);
71 | },
72 | addTo: function(object, parentKey, key) {
73 | key && (this.objects[key] = object);
74 | this.get(parentKey).add(object);
75 | },
76 | get: function(key) {
77 | return this.objects[key];
78 | },
79 | remove: function(o) {
80 | var object;
81 |
82 | if (typeof o === 'string') {
83 | object = this.objects[o];
84 | }
85 | else {
86 | object = o;
87 | }
88 |
89 | if (object) {
90 | object.parent.remove(object);
91 | delete this.objects[o];
92 | }
93 | },
94 | tick: function() {
95 | this.update();
96 | this.render();
97 | requestAnimationFrame(this.tick);
98 | },
99 | update: function() {
100 | this.updateCallbacks.forEach(function(callback) {callback()});
101 | },
102 | render: function() {
103 | this.renderer.render(this.scene, this.camera);
104 | },
105 | resize: function() {
106 | var width = window.innerWidth;
107 | var height = window.innerHeight;
108 |
109 | this.camera.aspect = width / height;
110 | this.camera.updateProjectionMatrix();
111 |
112 | this.renderer.setSize(width, height);
113 | this.resizeCallbacks.forEach(function(callback) {callback()});
114 | },
115 | initPostProcessing:function(passes) {
116 | var size = this.renderer.getSize();
117 | var pixelRatio = this.renderer.getPixelRatio();
118 | size.width *= pixelRatio;
119 | size.height *= pixelRatio;
120 |
121 | var composer = this.composer = new THREE.EffectComposer(this.renderer, new THREE.WebGLRenderTarget(size.width, size.height, {
122 | minFilter: THREE.LinearFilter,
123 | magFilter: THREE.LinearFilter,
124 | format: THREE.RGBAFormat,
125 | stencilBuffer: false
126 | }));
127 |
128 | var renderPass = new THREE.RenderPass(this.scene, this.camera);
129 | this.composer.addPass(renderPass);
130 |
131 | for (var i = 0; i < passes.length; i++) {
132 | var pass = passes[i];
133 | pass.renderToScreen = (i === passes.length - 1);
134 | this.composer.addPass(pass);
135 | }
136 |
137 | this.renderer.autoClear = false;
138 | this.render = function() {
139 | this.renderer.clear();
140 | this.composer.render();
141 | }.bind(this);
142 |
143 | this.addResizeCallback(function() {
144 | var width = window.innerWidth;
145 | var height = window.innerHeight;
146 |
147 | composer.setSize(width * pixelRatio, height * pixelRatio);
148 | }.bind(this));
149 | }
150 | };
151 |
--------------------------------------------------------------------------------
/examples/animated/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/examples/animated/main.js:
--------------------------------------------------------------------------------
1 | const colors = {
2 | turquoise: 0x47debd,
3 | darkPurple: 0x2e044e,
4 | purple: 0x7821ec,
5 | yellow: 0xfff95d,
6 | white: 0xffffff,
7 | black: 0x000000
8 | };
9 |
10 | const root = new THREERoot({
11 | createCameraControls: true,
12 | zNear: 0.01,
13 | zFar: 1000,
14 | antialias: true
15 | });
16 |
17 | root.renderer.shadowMap.enabled = true;
18 | root.renderer.setClearColor(colors.darkPurple);
19 | root.camera.position.set(-10, 0, 20);
20 |
21 | const light = new THREE.DirectionalLight(colors.turquoise);
22 | light.position.set(0.125, 1, 0);
23 | root.add(light);
24 |
25 | const light2 = new THREE.DirectionalLight(colors.yellow);
26 | light2.position.set(-0.125, -1, 0);
27 | root.add(light2);
28 |
29 | root.add(new THREE.AmbientLight(colors.purple));
30 |
31 | new THREE.JSONLoader().load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/304639/plus.json', (geometry) => {
32 | // test shapes, try different ones :D
33 | // model = new THREE.SphereGeometry(1, 16, 16);
34 | // model = new THREE.PlaneGeometry(40, 10, 200, 40);
35 | // model = new THREE.CylinderGeometry(2, 2, 8, 128, 64, true);
36 | // model = new THREE.TorusGeometry(8, 1, 128, 256);
37 | // model = new THREE.TorusKnotGeometry(2, 0.1, 64, 64, 3, 5);
38 |
39 | const fuzzy = new FuzzyMesh({
40 | geometry: geometry,
41 | // directions: model.vertices,
42 | config: {
43 | hairLength: 2,
44 | hairRadialSegments: 4,
45 | hairRadiusTop: 0.0,
46 | hairRadiusBase: 0.1,
47 | },
48 | materialUniformValues: {
49 | roughness: 1.0
50 | }
51 | });
52 | root.add(fuzzy);
53 | root.addUpdateCallback(() => {
54 | fuzzy.update();
55 | });
56 |
57 | const axes = [
58 | new THREE.Vector3(1, 0, 0),
59 | new THREE.Vector3(0, 1, 0),
60 | new THREE.Vector3(0, 0, 1),
61 | ];
62 |
63 | const proxy = {
64 | position: new THREE.Vector3(),
65 | angle: 0,
66 | };
67 |
68 | const tl = new TimelineMax({
69 | repeat: -1,
70 | delay: 1,
71 | repeatDelay: 1,
72 | onRepeat: () => {
73 | fuzzy.setRotationAxis(BAS.Utils.randomAxis());
74 | // fuzzy.setRotationAxis(axes[Math.random() * 3 | 0]);
75 | },
76 | onUpdate: () => {
77 | fuzzy.setPosition(proxy.position);
78 | fuzzy.setRotationAngle(proxy.angle);
79 | }
80 | });
81 |
82 | tl.to(proxy.position, 0.5, {y: 8, ease: Power2.easeOut});
83 | tl.to(proxy.position, 0.5, {y: 0, ease: Power2.easeIn});
84 | tl.to(proxy.position, 0.1, {y: -2, ease: Power2.easeOut});
85 | tl.to(proxy.position, 0.5, {y: 0, ease: Power2.easeOut});
86 | tl.fromTo(proxy, 1.0, {angle: 0}, {angle: Math.PI * 2 * (Math.random() > 0.5 ? 1 : -1), ease: Power1.easeInOut}, 0);
87 | });
88 |
--------------------------------------------------------------------------------
/examples/character/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/examples/character/main.js:
--------------------------------------------------------------------------------
1 |
2 | // hero class, based on work by Karim Maaloul
3 |
4 | function Hero() {
5 | this.runningCycle = 0;
6 | this.mesh = new THREE.Group();
7 | this.body = new THREE.Group();
8 | this.mesh.add(this.body);
9 |
10 |
11 | this.head = new FuzzyMesh({
12 | geometry: new THREE.SphereGeometry(4, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.55),
13 | materialUniformValues: {
14 | roughness: 1.0
15 | },
16 | config: {
17 | hairLength: 6,
18 | hairRadiusBase: 0.5,
19 | hairRadialSegments: 6,
20 | gravity: 2,
21 | fuzz: 0.25,
22 | minForceFactor: 0.5,
23 | maxForceFactor: 0.75
24 | }
25 | });
26 | this.head.position.y = this.headAnchorY = 13;
27 | this.head.castShadow = true;
28 | this.head.setRotationAxis(new THREE.Vector3(1, 0, 0));
29 | this.body.add(this.head);
30 |
31 |
32 | this.torso = new FuzzyMesh({
33 | geometry: new THREE.SphereGeometry(3, 32, 16, 0, Math.PI * 2, Math.PI * 0.25, Math.PI * 0.70),
34 | materialUniformValues: {
35 | roughness: 1.0
36 | },
37 | config: {
38 | hairLength: 5,
39 | hairRadiusBase: 0.5,
40 | hairRadialSegments: 6,
41 | gravity: 2,
42 | fuzz: 0.5,
43 | minForceFactor: 1.0,
44 | maxForceFactor: 4.0,
45 | centrifugalForceFactor: 4,
46 | }
47 | });
48 | this.torso.position.y = this.torsoAnchorY = 9;
49 | this.body.add(this.torso);
50 |
51 |
52 | this.handR = new FuzzyMesh({
53 | geometry: new THREE.SphereGeometry(1, 12, 12),
54 | materialUniformValues: {
55 | roughness: 1.0
56 | },
57 | config: {
58 | hairLength: 2,
59 | hairRadiusBase: 0.25,
60 | hairRadialSegments: 6,
61 | gravity: 2,
62 | fuzz: 0.25,
63 | }
64 | });
65 | this.handR.position.y = this.handAnchorY = 8;
66 | this.handR.position.z = this.handAnchorZ = 6;
67 | this.handR.setRotationAxis(new THREE.Vector3(0, 0, 1));
68 | this.body.add(this.handR);
69 |
70 |
71 | this.handL = new FuzzyMesh({
72 | geometry: new THREE.SphereGeometry(1, 12, 12),
73 | materialUniformValues: {
74 | roughness: 1.0
75 | },
76 | config: {
77 | hairLength: 2,
78 | hairRadiusBase: 0.25,
79 | hairRadialSegments: 6,
80 | gravity: 2,
81 | fuzz: 0.25,
82 | }
83 | });
84 | this.handL.position.y = this.handAnchorY;
85 | this.handL.position.z = -this.handAnchorZ;
86 | this.handL.setRotationAxis(new THREE.Vector3(0, 0, 1));
87 | this.body.add(this.handL);
88 |
89 |
90 | this.legR = new FuzzyMesh({
91 | geometry: new THREE.SphereGeometry(2, 48, 16, 0, Math.PI * 2, 0, Math.PI * 0.5),
92 | materialUniformValues: {
93 | roughness: 1.0,
94 | side: THREE.DoubleSide
95 | },
96 | config: {
97 | hairLength: 2,
98 | hairRadiusBase: 0.5,
99 | hairRadialSegments: 6,
100 | gravity: 1,
101 | fuzz: 0.25,
102 | }
103 | });
104 | this.legR.position.z = this.legAnchorZ = 3;
105 | this.legR.setRotationAxis(new THREE.Vector3(0, 0, 1));
106 | this.body.add(this.legR);
107 |
108 |
109 | this.legL = new FuzzyMesh({
110 | geometry: new THREE.SphereGeometry(2, 48, 16, 0, Math.PI * 2, 0, Math.PI * 0.5),
111 | materialUniformValues: {
112 | roughness: 1.0,
113 | side: THREE.DoubleSide
114 | },
115 | config: {
116 | hairLength: 2,
117 | hairRadiusBase: 0.5,
118 | hairRadialSegments: 6,
119 | gravity: 1,
120 | fuzz: 0.25,
121 | }
122 | });
123 | this.legL.position.z = -this.legAnchorZ;
124 | this.legL.setRotationAxis(new THREE.Vector3(0, 0, 1));
125 | this.body.add(this.legL);
126 |
127 |
128 | const color = new THREE.Color().setHSL(Math.random(), 0.75, 0.5);
129 | this.head.setColor(color);
130 | this.torso.setColor(color);
131 | this.handR.setColor(color);
132 | this.handL.setColor(color);
133 | this.legR.setColor(color);
134 | this.legL.setColor(color);
135 |
136 | this.tempV = new THREE.Vector3();
137 | }
138 |
139 | Hero.prototype.run = function(){
140 | var s = 0.125;
141 | var t = this.runningCycle;
142 | var amp = 4;
143 |
144 | t = t % (2*Math.PI);
145 |
146 | this.runningCycle += s;
147 |
148 | this.head.setPosition(this.tempV.set(
149 | this.head.position.x,
150 | this.headAnchorY - Math.cos( t * 2 ) * amp * .3,
151 | this.head.position.z
152 | ));
153 | this.head.setRotationAngle(Math.cos(t) * amp * .02);
154 |
155 | this.torso.setPosition(this.tempV.set(
156 | this.torso.position.x,
157 | this.torsoAnchorY - Math.cos( t * 2 ) * amp * .2,
158 | this.torso.position.z
159 | ));
160 | this.torso.setRotationAngle(-Math.cos( t + Math.PI ) * amp * .05);
161 |
162 | this.handR.setPosition(this.tempV.set(
163 | -Math.cos( t ) * amp,
164 | this.handR.position.y,
165 | this.handR.position.z
166 | ));
167 | this.handR.setRotationAngle(-Math.cos(t) * Math.PI / 8);
168 |
169 | this.handL.setPosition(this.tempV.set(
170 | -Math.cos( t + Math.PI) * amp,
171 | this.handL.position.y,
172 | this.handL.position.z
173 | ));
174 | this.handL.setRotationAngle(-Math.cos(t + Math.PI) * Math.PI / 8);
175 |
176 | this.legR.setPosition(this.tempV.set(
177 | Math.cos(t) * amp,
178 | Math.max(0, -Math.sin(t) * amp),
179 | this.legAnchorZ
180 | ));
181 |
182 | this.legL.setPosition(this.tempV.set(
183 | Math.cos(t + Math.PI) * amp,
184 | Math.max(0, -Math.sin(t + Math.PI) * amp),
185 | -this.legAnchorZ
186 | ));
187 |
188 | if (t > Math.PI){
189 | this.legR.setRotationAngle(Math.cos(t * 2 + Math.PI / 2) * Math.PI / 4);
190 | this.legL.setRotationAngle(0);
191 | }
192 | else {
193 | this.legR.setRotationAngle(0);
194 | this.legL.setRotationAngle(Math.cos(t * 2 + Math.PI / 2) * Math.PI / 4);
195 | }
196 |
197 | this.torso.update();
198 | this.head.update();
199 | this.handL.update();
200 | this.handR.update();
201 | this.legL.update();
202 | this.legR.update();
203 | };
204 |
205 | // scene stuff
206 |
207 | const root = new THREERoot({
208 | createCameraControls: true,
209 | zNear: 0.01,
210 | zFar: 1000,
211 | antialias: true
212 | });
213 |
214 | root.renderer.setClearColor(0xf1f1f1);
215 | root.controls.autoRotate = true;
216 | root.controls.autoRotateSpeed = -6;
217 | root.camera.position.set(30, 10, 30);
218 | root.scene.fog = new THREE.FogExp2(0xf1f1f1, 0.01);
219 |
220 | const light = new THREE.DirectionalLight(0xffffff, 1);
221 | light.position.set(0, 1, 0);
222 | root.add(light);
223 |
224 | const light2 = new THREE.DirectionalLight(0xffffff, 1);
225 | light2.position.set(0, -1, 0);
226 | root.add(light2);
227 |
228 | root.add(new THREE.AmbientLight(0xaaaaaa));
229 |
230 | const hero = new Hero();
231 | hero.mesh.position.y = -8;
232 | root.add(hero.mesh);
233 |
234 | root.addUpdateCallback(() => {
235 | hero.run();
236 | });
237 |
238 | const floor = new THREE.Mesh(
239 | new THREE.PlaneGeometry(400, 400),
240 | new THREE.MeshBasicMaterial({
241 | color: 0xcccccc
242 | })
243 | );
244 | floor.position.y = -8;
245 | floor.rotation.x = -Math.PI * 0.5;
246 | root.add(floor);
247 |
248 |
--------------------------------------------------------------------------------
/examples/text/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/examples/text/main.js:
--------------------------------------------------------------------------------
1 |
2 | // set the correct triangulate function for generating text geometries
3 |
4 | THREE.ShapeUtils.triangulateShape = function ( contour, holes ) {
5 | function removeDupEndPts( points ) {
6 | var l = points.length;
7 | if ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {
8 | points.pop();
9 | }
10 | }
11 | function addContour( vertices, contour ) {
12 | for ( var i = 0; i < contour.length; i ++ ) {
13 | vertices.push( contour[ i ].x );
14 | vertices.push( contour[ i ].y );
15 | }
16 | }
17 | removeDupEndPts( contour );
18 | holes.forEach( removeDupEndPts );
19 | var vertices = [];
20 | addContour( vertices, contour );
21 | var holeIndices = [];
22 | var holeIndex = contour.length;
23 | for ( i = 0; i < holes.length; i ++ ) {
24 | holeIndices.push( holeIndex );
25 | holeIndex += holes[ i ].length;
26 | addContour( vertices, holes[ i ] );
27 | }
28 | var result = earcut( vertices, holeIndices, 2 );
29 | var grouped = [];
30 | for ( var i = 0; i < result.length; i += 3 ) {
31 | grouped.push( result.slice( i, i + 3 ) );
32 | }
33 | return grouped;
34 | };
35 |
36 | // scene stuff
37 |
38 | const root = new THREERoot({
39 | createCameraControls: true,
40 | zNear: 0.01,
41 | zFar: 1000,
42 | antialias: true
43 | });
44 |
45 | root.renderer.setClearColor(0x000000);
46 | // root.controls.autoRotate = true;
47 | // root.controls.autoRotateSpeed = -6;
48 | root.camera.position.set(0, 0, 60);
49 | root.scene.fog = new THREE.FogExp2(0xf1f1f1, 0.001);
50 |
51 | const light = new THREE.DirectionalLight(0xffffff, 0.75);
52 | light.position.set(1, 1, 1);
53 | root.add(light);
54 |
55 | const light2 = new THREE.DirectionalLight(0xffffff, 0.75);
56 | light2.position.set(-1, 1, 1);
57 | root.add(light2);
58 |
59 | // root.add(new THREE.AmbientLight(0x888888));
60 |
61 | // text stuff
62 | const string = 'CODEVEMBER';
63 | const fontUrl = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/droid_sans_bold.typeface.js';
64 | const fontParams = {
65 | size: 12,
66 | height: 1,
67 | curveSegments: 1,
68 |
69 | bevelEnabled: false,
70 | bevelThickness: 1,
71 | bevelSize: 1,
72 | material: 0,
73 | extrudeMaterial: 0,
74 |
75 | letterSpacing: 200
76 | };
77 |
78 | new THREE.FontLoader().load(fontUrl, (font) => {
79 | const letterMeshes = string.split('').map((letter) => {
80 | return createLetterMesh(letter, font);
81 | });
82 |
83 | const letterGroup = new THREE.Group();
84 | root.add(letterGroup);
85 |
86 | let offsetX = 0;
87 | letterMeshes.forEach((mesh, i) => {
88 | letterGroup.add(mesh);
89 |
90 | mesh.position.x = offsetX;
91 | offsetX += mesh.userData.ha;
92 |
93 | mesh.setColor(new THREE.Color().setHSL(i / letterMeshes.length, 1.0, 0.5));
94 | });
95 |
96 | const bounds = new THREE.Box3();
97 |
98 | bounds.setFromObject(letterGroup);
99 |
100 | const size = bounds.getSize();
101 |
102 | letterGroup.position.x = -size.x * 0.5;
103 |
104 | const v = new THREE.Vector3();
105 | let t = 0;
106 |
107 | root.addUpdateCallback(() => {
108 | letterGroup.children.forEach((child, i) => {
109 | v.copy(child.position);
110 | v.y = (Math.sin((t + i) * 1.2)) * 5;
111 |
112 | child.setPosition(v);
113 | child.update();
114 |
115 | t += (1/60);
116 | });
117 | });
118 | });
119 |
120 | function createLetterMesh(char, font) {
121 | const geometry = new THREE.TextGeometry(char, {
122 | font,
123 | ...fontParams
124 | });
125 |
126 | // geometry.center();
127 |
128 | const modifier = new THREE.TessellateModifier(1);
129 | for (let i = 0; i < 6; i++) {
130 | modifier.modify(geometry);
131 | }
132 |
133 | const mesh = new FuzzyMesh({
134 | geometry,
135 | config: {
136 | hairLength: 3,
137 | hairRadiusBase: 0.20,
138 | hairRadiusTop: 0.20,
139 | hairRadialSegments: 4,
140 | fuzz: 2,
141 | gravity: 4,
142 | minForceFactor: 0.5,
143 | maxForceFactor: 1.0,
144 | movementForceFactor: 0.9
145 | },
146 | materialUniformValues: {
147 | roughness: 0.4,
148 | metalness: 0.1
149 | }
150 | });
151 |
152 | const scale = fontParams.size / font.data.resolution;
153 | const glyph = font.data.glyphs[char];
154 |
155 | // todo: this doesn't feel like the correct way to calculate letter spacing
156 | mesh.userData.ha = glyph.ha * scale + fontParams.letterSpacing * scale;
157 |
158 | return mesh;
159 | }
160 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fuzzy_geometries",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "three": {
8 | "version": "0.87.1",
9 | "resolved": "https://npm.dpdk.com/three/-/three-0.87.1.tgz",
10 | "integrity": "sha1-Rmo07cRUNFnO2bnX0na2Uhb+K6g="
11 | },
12 | "three-bas": {
13 | "version": "2.2.0",
14 | "resolved": "https://npm.dpdk.com/three-bas/-/three-bas-2.2.0.tgz",
15 | "integrity": "sha1-3lFveqbZcAd3+13cBVdbMKK6Sa8=",
16 | "requires": {
17 | "three": "0.87.1"
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fuzzy_geometries",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "",
6 | "scripts": {
7 | "dev": "./bin/node_modules/live-server --watch=src,examples"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "three": "^0.87.1",
13 | "three-bas": "^2.2.0"
14 | },
15 | "devDependencies": {
16 | "live-server": "^1.2.0",
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/FuzzyMesh.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a fuzzy mesh!
3 | * @param params
4 | * @constructor
5 | */
6 | function FuzzyMesh(params) {
7 | const config = this.config = {
8 | recursiveRotation: true,
9 | hairLength: 1,
10 | hairRadialSegments: 3,
11 | hairHeightSegments: 16,
12 | hairRadiusTop: 0.0,
13 | hairRadiusBase: 0.1,
14 | minForceFactor: 1.0,
15 | maxForceFactor: 1.0,
16 | fuzz: 0.25,
17 | gravity: 1.0,
18 | centrifugalForceFactor: 1,
19 | centrifugalDecay: 0.8,
20 | movementForceFactor: 0.75,
21 | movementDecay: 0.7,
22 | settleDecay: 0.97, // should always be higher than movementDecay and centrifugal decay
23 | ...params.config
24 | };
25 | const materialUniformValues = {
26 | metalness: 0.5,
27 | roughness: 0.5,
28 | ...params.materialUniformValues
29 | };
30 | const positions = params.geometry.vertices;
31 |
32 | // create a cone prefab for pointy hair
33 | // create a cylinder prefab for non-pointy hair
34 | let prefab;
35 |
36 | if (config.hairRadiusTop === 0) {
37 | prefab = new THREE.ConeGeometry(
38 | config.hairRadiusBase,
39 | config.hairLength,
40 | config.hairRadialSegments,
41 | config.hairHeightSegments,
42 | true
43 | );
44 | }
45 | else {
46 | prefab = new THREE.CylinderGeometry(
47 | config.hairRadiusTop,
48 | config.hairRadiusBase,
49 | config.hairLength,
50 | config.hairRadialSegments,
51 | config.hairHeightSegments,
52 | false
53 | );
54 | }
55 | // cone and cylinder geometries are created around the center
56 | // translate them so the vertices start at y=0 and move up
57 | prefab.translate(0, config.hairLength * 0.5, 0);
58 |
59 | // create a geometry with 1 prefab per vertex of the supplied geometry
60 | const geometry = new BAS.PrefabBufferGeometry(prefab, positions.length);
61 |
62 | // forceFactor is a scalar that multiplies the total force affecting the vertex
63 | geometry.createAttribute('forceFactor', 1, (data) => {
64 | data[0] = THREE.Math.randFloat(config.minForceFactor, config.maxForceFactor);
65 | });
66 |
67 | // settleOffset is used to make sure the hair don't stop moving at the same time
68 | geometry.createAttribute('settleOffset', 1, (data) => {
69 | data[0] = THREE.Math.randFloat(0, Math.PI * 2);
70 | });
71 |
72 | // hair positions based on model vertices
73 | geometry.createAttribute('hairPosition', 3, (data, i) => {
74 | positions[i].toArray(data);
75 | });
76 |
77 | // hair directions
78 | let directions;
79 |
80 | if (params.directions) {
81 | directions = params.directions;
82 | }
83 | // if params.directions is not set, we use vertex normals instead
84 | else {
85 | directions = [];
86 |
87 | params.geometry.computeVertexNormals();
88 |
89 | // get a flat array of vertex normals
90 | for (let i = 0; i < params.geometry.faces.length; i++) {
91 | const face = params.geometry.faces[i];
92 |
93 | directions[face.a] = face.vertexNormals[0];
94 | directions[face.b] = face.vertexNormals[1];
95 | directions[face.c] = face.vertexNormals[2];
96 | }
97 | }
98 |
99 | // base hair directions (which direction the hair goes with no force applied to it)
100 | const direction = new THREE.Vector3();
101 |
102 | geometry.createAttribute('baseDirection', 3, (data, i) => {
103 | direction.copy(directions[i]);
104 | direction.x += THREE.Math.randFloatSpread(config.fuzz);
105 | direction.y += THREE.Math.randFloatSpread(config.fuzz);
106 | direction.z += THREE.Math.randFloatSpread(config.fuzz);
107 | direction.normalize();
108 | direction.toArray(data);
109 | });
110 |
111 | const simpleShader = `
112 | float f = position.y / HAIR_LENGTH;
113 |
114 | vec3 totalForce = globalForce;
115 |
116 | totalForce *= 1.0 - (sin(settleTime + settleOffset) * 0.05 * settleScale);
117 | totalForce += hairPosition * centrifugalDirection * centrifugalForce;
118 | totalForce *= forceFactor;
119 |
120 | vec3 to = normalize(baseDirection + totalForce * f);
121 | vec4 quat = quatFromUnitVectors(UP, to);
122 |
123 | transformed = rotateVector(quat, transformed) + hairPosition;
124 | `;
125 |
126 | const recursiveShader = `
127 | // accumulator for total force
128 | vec3 totalForce = globalForce;
129 | // add a little offset so the hairs don't all stop moving at the same time
130 | // settleScale is increased when forces are applied, then gradually goes back to zero
131 | totalForce *= 1.0 - (sin(settleTime + settleOffset) * 0.05 * settleScale);
132 | // add force based on rotation
133 | totalForce += hairPosition * centrifugalDirection * centrifugalForce;
134 | // scale force based on a magic number!
135 | totalForce *= forceFactor;
136 |
137 | // accumulator for position
138 | vec3 finalPosition = vec3(0.0, 0.0, 0.0);
139 | // get height fraction between 0.0 and 1.0
140 | float f = position.y / HAIR_LENGTH;
141 | // determine target position based on force and height fraction
142 | vec3 to = normalize(baseDirection + totalForce * f);
143 | // calculate quaterion needed to rotate UP to target rotation
144 | vec4 q = quatFromUnitVectors(UP, to);
145 | // only apply this rotation to position x and z
146 | // position y will be calculated in the loop below
147 | vec3 v = vec3(position.x, 0.0, position.z);
148 |
149 | finalPosition += rotateVector(q, v);
150 |
151 | // recursively calculate rotations using the same approach as above
152 | for (float i = 0.0; i < HAIR_LENGTH; i += SEGMENT_STEP) {
153 | if (position.y <= i) break;
154 |
155 | float f = i * FORCE_STEP;
156 | vec3 to = normalize(baseDirection + totalForce * f);
157 | vec4 q = quatFromUnitVectors(UP, to);
158 | // apply this rotation to a 'segment'
159 | vec3 v = vec3(0.0, SEGMENT_STEP, 0.0);
160 | // all segments leading up to the Y position are added to the final position
161 | finalPosition += rotateVector(q, v);
162 | }
163 |
164 | transformed = finalPosition + hairPosition;
165 | `;
166 |
167 | const material = new BAS.StandardAnimationMaterial({
168 | flatShading: true,
169 | wireframe: false,
170 | uniformValues: materialUniformValues,
171 | uniforms: {
172 | hairLength: {value: config.hairLength},
173 | settleTime: {value: 0.0},
174 | settleScale: {value: 1.0},
175 | globalForce: {value: new THREE.Vector3(0.0, -config.gravity, 0.0)},
176 | centrifugalForce: {value: 0.0},
177 | centrifugalDirection: {value: new THREE.Vector3(1, 0, 1).normalize()}
178 | },
179 | defines: {
180 | 'HAIR_LENGTH': (config.hairLength).toFixed(2),
181 | 'SEGMENT_STEP': (config.hairLength / config.hairHeightSegments).toFixed(2),
182 | 'FORCE_STEP': (1.0 / config.hairLength).toFixed(2)
183 | },
184 | vertexParameters: `
185 | uniform float hairLength;
186 | uniform float heightSteps;
187 | uniform float heightStepSize;
188 |
189 | uniform vec3 globalForce;
190 | uniform float centrifugalForce;
191 | uniform vec3 centrifugalDirection;
192 | uniform float settleTime;
193 | uniform float settleScale;
194 |
195 | attribute float forceFactor;
196 | attribute float settleOffset;
197 | attribute vec3 hairPosition;
198 | attribute vec3 baseDirection;
199 |
200 | vec3 UP = vec3(0.0, 1.0, 0.0);
201 | `,
202 | vertexFunctions: [
203 | BAS.ShaderChunk.quaternion_rotation,
204 | `
205 | // based on THREE.Quaternion.setFromUnitVectors
206 | // would be great if we can get rid of the conditionals
207 | vec4 quatFromUnitVectors(vec3 from, vec3 to) {
208 | vec3 v = vec3(0.0, 0.0, 0.0);
209 | float r = dot(from, to) + 1.0;
210 |
211 | if (r < 0.00001) {
212 | r = 0.0;
213 |
214 | if (abs(from.x) > abs(from.z)) {
215 | v.x = -from.y;
216 | v.y = from.x;
217 | v.z = 0.0;
218 | }
219 | else {
220 | v.x = 0.0;
221 | v.y = -from.z;
222 | v.z = from.y;
223 | }
224 | }
225 | else {
226 | v = cross(from, to);
227 | }
228 |
229 | return normalize(vec4(v.xyz, r));
230 | }
231 | `
232 | ],
233 | vertexPosition: config.recursiveRotation ? recursiveShader : simpleShader
234 | });
235 |
236 | THREE.Mesh.call(this, geometry, material);
237 |
238 | // since the bounding box for the hair is never updated,
239 | // set frustumCulled to false so the object doesn't disappear suddenly
240 | this.frustumCulled = false;
241 | // add the base geometry to self
242 | this.baseMesh = new THREE.Mesh(
243 | params.geometry,
244 | new THREE.MeshStandardMaterial(materialUniformValues)
245 | );
246 | this.add(this.baseMesh);
247 |
248 | // rotation stuff
249 | this._quat = new THREE.Quaternion();
250 | this.conjugate = new THREE.Quaternion();
251 | this.rotationAxis = new THREE.Vector3(0, 1, 0);
252 | this.angle = 0.0;
253 | this.previousAngle = this.angle;
254 |
255 | // position stuff
256 | this.previousPosition = this.position.clone();
257 | this.positionDelta = new THREE.Vector3();
258 | this.movementForce = new THREE.Vector3();
259 | }
260 |
261 | FuzzyMesh.prototype = Object.create(THREE.Mesh.prototype);
262 | FuzzyMesh.prototype.constructor = FuzzyMesh;
263 |
264 | FuzzyMesh.prototype.setColor = function(color) {
265 | this.baseMesh.material.color.set(color);
266 | this.material.uniforms.diffuse.value.set(color);
267 | };
268 |
269 | FuzzyMesh.prototype.setPosition = function(position) {
270 | this.previousPosition.copy(this.position);
271 | this.position.copy(position);
272 | };
273 |
274 | FuzzyMesh.prototype.setRotationAngle = function(angle) {
275 | this.previousAngle = this.angle;
276 | this.angle = angle;
277 | };
278 |
279 | FuzzyMesh.prototype.setRotationAxis = function(axis) {
280 | this.setRotationAngle(0);
281 |
282 | const ra = this.rotationAxis;
283 | const cd = this.material.uniforms.centrifugalDirection.value;
284 | const q = this._quat;
285 |
286 | // reset rotation axis and centrifugal direction;
287 | ra.set(0, 1, 0);
288 | cd.set(1, 0, 1);
289 |
290 | // get angle between default rotation axis and target rotation axis
291 | q.setFromUnitVectors(ra, axis);
292 | // apply angle to centrifugal direction
293 | cd.applyQuaternion(q);
294 | // normalize the angle, and make the values absolute
295 | cd.normalize();
296 | cd.x = Math.abs(cd.x);
297 | cd.y = Math.abs(cd.y);
298 | cd.z = Math.abs(cd.z);
299 | // finally don't forget to update the rotation axis
300 | ra.copy(axis);
301 | };
302 |
303 | FuzzyMesh.prototype.update = function() {
304 | // apply movement force
305 | this.positionDelta.copy(this.previousPosition).sub(this.position);
306 |
307 | this.movementForce.multiplyScalar(this.config.movementDecay);
308 | this.movementForce.x += this.positionDelta.x * this.config.movementForceFactor;
309 | this.movementForce.y += this.positionDelta.y * this.config.movementForceFactor;
310 | this.movementForce.z += this.positionDelta.z * this.config.movementForceFactor;
311 |
312 | this.material.uniforms.globalForce.value.set(
313 | this.movementForce.x,
314 | this.movementForce.y - this.config.gravity,
315 | this.movementForce.z
316 | );
317 |
318 | this.previousPosition.copy(this.position);
319 |
320 | // apply centrifugal force
321 | const rotationSpeed = Math.abs(this.previousAngle - this.angle) % (Math.PI * 2);
322 | this.material.uniforms.centrifugalForce.value *= this.config.centrifugalDecay;
323 | this.material.uniforms.centrifugalForce.value += rotationSpeed * this.config.centrifugalForceFactor;
324 |
325 | this.previousAngle = this.angle;
326 |
327 | // adjust global force based on rotation
328 | this.conjugate.copy(this.quaternion).conjugate();
329 | this.material.uniforms.globalForce.value.applyQuaternion(this.conjugate);
330 |
331 | // apply rotation to object
332 | this.quaternion.setFromAxisAngle(this.rotationAxis, this.angle);
333 |
334 | // rest / settle values
335 | this.material.uniforms.settleTime.value += (1/10);
336 | this.material.uniforms.settleScale.value *= this.config.settleDecay;
337 | this.material.uniforms.settleScale.value += (this.movementForce.length() + rotationSpeed) * 0.1;
338 | this.material.uniforms.settleScale.value > 1.0 && (this.material.uniforms.settleScale.value = 1.0);
339 | };
340 |
--------------------------------------------------------------------------------