diff --git a/.vscode/.browse.VC.db b/.vscode/.browse.VC.db new file mode 100644 index 0000000..8d29ece Binary files /dev/null and b/.vscode/.browse.VC.db differ diff --git a/.vscode/.browse.VC.db-wal b/.vscode/.browse.VC.db-wal new file mode 100644 index 0000000..feab6ed Binary files /dev/null and b/.vscode/.browse.VC.db-wal differ diff --git a/README.md b/README.md index 1410c30..d9fd9a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,29 @@ # [Project2: Toolbox Functions](https://github.com/CIS700-Procedural-Graphics/Project2-Toolbox-Functions) +## Description + +I'm not a very good sketcher, so this project was pretty challenging. I spent a lot of time browsing realistic images of wings, feather distributions, and flapping patterns, and struggled to bring a lot of them to life in my implementation. That being said, I learned a lot about toolbox functions in this project, which was cool. + +#### Wing shape + +The shape of the wing is a cubic spline, with two of the endpoints connected with a straight line to make a curved 3D plane. It's not the most realistic shape of a wing, but I thought it looked neat. I added GUI functionality to modify the 4 control points of the spline so that I could quickly move the points around and create the curve shape I wanted. + +#### Wing flapping + +I defined five stroke "positions" (various low/mid/high strokes) and attempted to make my wing flap by interpolating my spline's control points to the various flap positions. That is, if my wing begins in stroke position 1, it first interpolates to stroke position 2, then stroke position 3, and so on. These stroke positions are defined in the `coordinates.js` file. Given that I am using basic LERP to transform my spline, the flapping motion looks as though it is broken down into five steps, unfortunately. I tried to use smooth step & sine curves to make the flapping smoother, to no avail. If given more time, I would have defined another cubic spline by the five stroke position points that I would have moved my wing along. + +#### Feathers + +Feathers are positioned around the curved portion of the wing in three separate layers. Each layer has customizable sizes, colors, and feather distributions. One of the things I struggled with was getting the feathers to "point" in the correct direction. Given I flap my wing by moving my spline's control points, the feathers occasionally point in an incorrect direction, and it's pretty difficult to calculate the correct axis and angle to use to rotate each feather during flapping. In hindsight, I would have **not** used control points to flap my wing, and instead used normal matrix rotations & translations. I would have constructed a scene-graph implementation where the feathers are children of the wing root node. This way, the feathers would trivially point in the correct direction, regardless of the wing's overall rotation. + +#### Wind + +Based upon the wind direction and strength, I vibrate the feathers in a specific direction. + +#### GUI + +Many, many aspects of this implementation can be modified in the GUI. This includes (1) which portions of the wing are visible (control points, outline, feathers, etc...), (2) whether or not the wing is flapping or wind exists, (3) various feather customizations, and (4) the wing's overall shape. + ## Overview The objective of this assignment is to procedurally model and animate a bird wing. Let's get creative! @@ -18,7 +42,7 @@ Begin with a 3D curve for your basic wing shape. Three.js provides classes to cr ##### Distribute feathers -We have provided a simple feather model from which to begin. You are not required to use this model if you have others that you prefer. From this base, you must duplicate the feather to model a complete wing, and your wing should consist of at least thirty feathers. Distribute points along the curve you created previously; you will append the feather primitives to the curve at these points. Make sure that you modify the size, orientation, and color of your feathers depending on their location on the wing. +We have provided a simple feather model from which to begin. You are not required to use this model if you have others that you prefer. From this base, you must duplicate the feather to model a complete wing, and your wing should consist of at least thirty feathers. Distribute points along the curve you created previously; you will append the feather primitives to the curve at these points. Make sure that you modify the size, orientation, and color of your feathers depending on their location on the wing. Feel free to diversify your wings by using multiple base feather models. diff --git a/package.json b/package.json index c80e8a3..cde25c8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "scripts": { "start": "webpack-dev-server --hot --inline", "build": "webpack", - "deploy": "rm -rf npm-debug.log && git checkout master && git commit -am 'update' && gh-pages-deploy" + "deploy": "gh-pages-deploy" }, "gh-pages-deploy": { "prep": [ diff --git a/references/wing-flapping-1.gif b/references/wing-flapping-1.gif new file mode 100644 index 0000000..895949e Binary files /dev/null and b/references/wing-flapping-1.gif differ diff --git a/references/wing-flapping-2.gif b/references/wing-flapping-2.gif new file mode 100644 index 0000000..7b667fd Binary files /dev/null and b/references/wing-flapping-2.gif differ diff --git a/references/wing-flapping-3.gif b/references/wing-flapping-3.gif new file mode 100644 index 0000000..9beec93 Binary files /dev/null and b/references/wing-flapping-3.gif differ diff --git a/src/coordinates.js b/src/coordinates.js new file mode 100644 index 0000000..8f0241d --- /dev/null +++ b/src/coordinates.js @@ -0,0 +1,32 @@ +module.exports = { + A: { + A: { x: -20, y: 0, z: 0 }, + B: { x: -5, y: -15, z: 0 }, + C: { x: 20, y: -15, z: 0 }, + D: { x: 10, y: 0, z: 0 } + }, + B: { + A: { x: 0, y: 0, z: -25 }, + B: { x: 10, y: 0, z: -20 }, + C: { x: 20, y: 0, z: -5 }, + D: { x: 10, y: 0, z: 0 } + }, + C: { + A: { x: 5, y: 0, z: -15 }, + B: { x: 10, y: 0, z: -10 }, + C: { x: 20, y: 0, z: -5 }, + D: { x: 10, y: 0, z: 0 } + }, + D: { + A: { x: -20, y: 0, z: 0 }, + B: { x: -5, y: -15, z: 0 }, + C: { x: 20, y: -15, z: 0 }, + D: { x: 10, y: 0, z: 0 } + }, + E: { + A: { x: 0, y: 0, z: 25 }, + B: { x: 10, y: 0, z: 20 }, + C: { x: 20, y: 0, z: 5 }, + D: { x: 10, y: 0, z: 0 } + } +} \ No newline at end of file diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..8d6ceba --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,16 @@ +module.exports = { + smoothstep: function (a, b, t) { + var c = Math.max(0, Math.min(1, (t - a) / (b - a))); + return c * c * (3 - 2 * c); + }, + + smootherstep: function (a, b, t) { + var c = Math.max(0, Math.min(1, (t - a) / (b - a))); + return c * c * c * (c * (c * 6 - 15) + 10); + }, + + bezier: function (a, b, c, t) { + var c = (a * t * t) + (b * 2 * t * (1 - t)) + (c * (1 - t) * (1 - t)); + return c; + } +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index fd8fbd4..128059c 100755 --- a/src/main.js +++ b/src/main.js @@ -1,73 +1,379 @@ // Skybox texture from: https://github.com/mrdoob/three.js/tree/master/examples/textures/cube/skybox -const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much +const THREE = require('three'); +const _ = require('lodash'); + import Framework from './framework' +import Helpers from './helpers' +import Coords from './coordinates' -// called after the scene loads -function onLoad(framework) { - var scene = framework.scene; - var camera = framework.camera; - var renderer = framework.renderer; - var gui = framework.gui; - var stats = framework.stats; - - // Basic Lambert white - var lambertWhite = new THREE.MeshLambertMaterial({ color: 0xaaaaaa, side: THREE.DoubleSide }); - - // Set light - var directionalLight = new THREE.DirectionalLight( 0xffffff, 1 ); - directionalLight.color.setHSL(0.1, 1, 0.95); - directionalLight.position.set(1, 3, 2); - directionalLight.position.multiplyScalar(10); +var mainSettingsGUI = { + flap: { + val: true, + name: 'Flap wing' + }, + flapSpeed: { + val: 2.0, + name: 'Flap speed' + }, + showFeathers: { + val: true, + name: 'Show feathers' + }, + showWingCurve: { + val: false, + name: 'Show shape' + }, + showControlPoints: { + val: false, + name: 'Show ctrl points' + }, + showWingOutline: { + val: false, + name: 'Show outline' + } +}; + +var windSettingsGUI = { + directionX: { + val: false, + name: 'X direction' + }, + directionY: { + val: false, + name: 'Y direction' + }, + directionZ: { + val: true, + name: 'Z direction' + }, + strength: { + val: 1.0, + name: 'Strength' + } +}; + +var featherSettingsGUI = { + firstLayer: { + scale: 6.0, + count: 30, + distribution: new THREE.Vector3(0.0, 0.0, 0.0), + color: 0xFD6E4C + }, + secondLayer: { + scale: 6.0, + count: 30, + distribution: new THREE.Vector3(-5.0, 0.0, 0.0), + color: 0xFEB251 + }, + thirdLayer: { + scale: 2.0, + count: 60, + distribution: new THREE.Vector3(-5.0, 0.0, 1.0), + color: 0xFFE456 + } +} + +var loadedFeather; + +var pointsObj; +var pointsMatParams = { + size: 1, + sizeAttenuation: true, + color: 0x0000ff +}; + +var curveObj; +var curveMatParams = { + side: THREE.DoubleSide, + wireframe: false +}; + +var outlineObj; +var outlineMatParams = { + color: 0x0000ff, + linewidth: 10 +}; + +var featherObjs = []; +var featherMatParams = { + side: THREE.DoubleSide +}; +var featherLayout = [ + featherSettingsGUI.firstLayer, + featherSettingsGUI.secondLayer, + featherSettingsGUI.thirdLayer +]; + +var coordsFlap = [ Coords.A, Coords.B, Coords.C, Coords.D, Coords.E ]; +var coordsFlapIndex = 0; +var coords = _.cloneDeep(coordsFlap[0]); + +function renderPoints () { + var pointsGeom = new THREE.Geometry(); + var points = []; + + _.each(coords, coord => { + var point = new THREE.Vector3(coord.x, coord.y, coord.z); + pointsGeom.vertices.push(point); + }); + + var pointsMat = new THREE.PointsMaterial(pointsMatParams); + + pointsObj = new THREE.Points(pointsGeom, pointsMat); +} + +function renderCurve () { + var points = []; + _.each(coords, coord => { + var point = new THREE.Vector3(coord.x, coord.y, coord.z); + points.push(point); + }); + + var bezCurve = new THREE.CubicBezierCurve3(points[0], points[1], points[2], points[3]); + var curve = new THREE.CurvePath(); + curve.add(bezCurve); + curve.closePath(); + + var numPoints = 50; + var curveGeom = curve.createPointsGeometry(numPoints); + + for (var i = 0; i < numPoints - 1; i++) { + curveGeom.faces.push(new THREE.Face3(0, i + 1, i + 2)); + } + + curveGeom.computeFaceNormals(); + curveGeom.computeVertexNormals(); + + var curveMat = new THREE.MeshNormalMaterial(curveMatParams); + + curveObj = new THREE.Mesh(curveGeom, curveMat); +} + +function renderOutline () { + var points = []; + _.each(coords, coord => { + var point = new THREE.Vector3(coord.x, coord.y, coord.z); + points.push(point); + }); + + var bezCurve = new THREE.CubicBezierCurve3(points[0], points[1], points[2], points[3]); + var curve = new THREE.CurvePath(); + curve.add(bezCurve); + curve.closePath(); + + var outlineGeom = new THREE.Geometry(); + var numPoints = 50; + + outlineGeom.vertices = curve.getPoints(numPoints); + + var outlineMat = new THREE.LineBasicMaterial(outlineMatParams); + + outlineObj = new THREE.Line(outlineGeom, outlineMat); +} + +function renderFeathers () { + var points = []; + _.each(coords, coord => { + var point = new THREE.Vector3(coord.x, coord.y, coord.z); + points.push(point); + }); + + var bezCurve = new THREE.CubicBezierCurve3(points[0], points[1], points[2], points[3]); + + featherObjs = []; + + if (loadedFeather) { + _.each(featherLayout, layout => { + var count = layout.count; + var distr = layout.distribution; + var scale = layout.scale; + var color = layout.color; + + var vertices = bezCurve.getPoints(count); + var scaleFactor = 1.0; + + _.each(vertices, (vertex, i) => { + var featherObj = loadedFeather.clone(); + + featherObj.name = 'feather'; + featherObj.position.set(vertex.x + distr.x, vertex.y + distr.y, vertex.z + distr.z); + featherObj.scale.set(scale, scale, scale); + + featherObj.traverse(child => { + if (child instanceof THREE.Mesh) { + var featherMat = new THREE.MeshBasicMaterial(featherMatParams); + featherMat.color = new THREE.Color(color); + child.material = featherMat; + } + }); - // set skybox + featherObjs.push(featherObj); + + scale *= scaleFactor; + }); + }); + } +}; + +function renderWind () { + _.each(featherObjs, featherObj => { + var date = new Date(); + var t = Math.sin(date.getTime() / 100) * 2 * Math.PI / 180; + var strength = windSettingsGUI.strength.val; + + if (windSettingsGUI.directionX.val) { + featherObj.rotateX(t * strength); + } + + if (windSettingsGUI.directionY.val) { + featherObj.rotateY(t * strength); + } + + if (windSettingsGUI.directionZ.val) { + featherObj.rotateZ(t * strength); + } + }); +}; + +function renderWing (framework) { + var { scene } = framework; + + scene.remove(curveObj); + scene.remove(pointsObj); + scene.remove(outlineObj); + + _.each(featherObjs, featherObj => { + scene.remove(featherObj); + }); + + renderCurve(); + renderPoints(); + renderOutline(); + renderFeathers(); + renderWind(); + + if (mainSettingsGUI.showWingCurve.val) { + scene.add(curveObj); + } + + if (mainSettingsGUI.showControlPoints.val) { + scene.add(pointsObj); + } + + if (mainSettingsGUI.showFeathers.val) { + _.each(featherObjs, featherObj => { + scene.add(featherObj); + }); + } + + if (mainSettingsGUI.showWingOutline.val) { + scene.add(outlineObj); + } +} + +function loadFeather (framework) { + var loader = new THREE.OBJLoader(); + var urlPrefix = 'https://raw.githubusercontent.com/zelliott/Project2-Toolbox-Functions/master/geo/feather.obj'; + + loader.load(urlPrefix, feather => { + loadedFeather = feather; + renderWing(framework); + }); +} + +function loadSkybox (scene) { var loader = new THREE.CubeTextureLoader(); - var urlPrefix = '/images/skymap/'; + var urlPrefix = './images/skymap/'; + var urlSuffix = ''; var skymap = new THREE.CubeTextureLoader().load([ - urlPrefix + 'px.jpg', urlPrefix + 'nx.jpg', - urlPrefix + 'py.jpg', urlPrefix + 'ny.jpg', - urlPrefix + 'pz.jpg', urlPrefix + 'nz.jpg' - ] ); + urlPrefix + 'px.jpg' + urlSuffix, urlPrefix + 'nx.jpg' + urlSuffix, + urlPrefix + 'py.jpg' + urlSuffix, urlPrefix + 'ny.jpg' + urlSuffix, + urlPrefix + 'pz.jpg' + urlSuffix, urlPrefix + 'nz.jpg' + urlSuffix + ]); scene.background = skymap; +} - // load a simple obj mesh - var objLoader = new THREE.OBJLoader(); - objLoader.load('/geo/feather.obj', function(obj) { +// called after the scene loads +function onLoad (framework) { + var { scene, camera, renderer, gui, stats } = framework; + var directionalLight = new THREE.DirectionalLight(0xffffff, 1); - // LOOK: This function runs after the obj has finished loading - var featherGeo = obj.children[0].geometry; + directionalLight.color.setHSL(0.1, 1, 0.95); + directionalLight.position.set(1, 3, 20); + directionalLight.position.multiplyScalar(10); - var featherMesh = new THREE.Mesh(featherGeo, lambertWhite); - featherMesh.name = "feather"; - scene.add(featherMesh); - }); + loadFeather(framework); + loadSkybox(scene); - // set camera position - camera.position.set(0, 1, 5); + // Camera + camera.position.set(0, -40, 0); camera.lookAt(new THREE.Vector3(0,0,0)); - // scene.add(lambertCube); scene.add(directionalLight); - // edit params and listen to changes like this - // more information here: https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage + renderWing(framework); + gui.add(camera, 'fov', 0, 180).onChange(function(newVal) { camera.updateProjectionMatrix(); }); + + var guiMainFolder = gui.addFolder('Main settings'); + _.each(mainSettingsGUI, (val, key) => { + guiMainFolder.add(mainSettingsGUI[key], 'val').name(val.name).onChange(function () { renderWing(framework); }); + }); + + var guiWindFolder = gui.addFolder('Wind settings'); + _.each(windSettingsGUI, (val, key) => { + guiWindFolder.add(windSettingsGUI[key], 'val').name(val.name).onChange(function () { renderWind(framework); }); + }); + + var guiFeatherFolder = gui.addFolder('Feather settings'); + _.each(featherSettingsGUI, (val, key) => { + guiFeatherFolder.add(featherSettingsGUI[key], 'scale').name('Scale').onChange(function () { renderWing(framework); }); + guiFeatherFolder.add(featherSettingsGUI[key], 'count').name('Count').onChange(function () { renderWing(framework); }); + guiFeatherFolder.addColor(featherSettingsGUI[key], 'color').name('Color').onChange(function () { renderWing(framework); }); + }); + + var guiWingCurveFolder = gui.addFolder('Change wing shape'); + _.each(coords, (val, key) => { + guiWingCurveFolder.add(val, 'x', -40, 40).name('Point ' + key + ': x') + .onChange(function () { renderWing(framework); }); + guiWingCurveFolder.add(val, 'y', -40, 40).name('Point ' + key + ': y') + .onChange(function () { renderWing(framework); }); + guiWingCurveFolder.add(val, 'z', -40, 40).name('Point ' + key + ': z') + .onChange(function () { renderWing(framework); }); + }); } -// called on frame updates +var t = 0; + function onUpdate(framework) { - var feather = framework.scene.getObjectByName("feather"); - if (feather !== undefined) { - // Simply flap wing - var date = new Date(); - feather.rotateZ(Math.sin(date.getTime() / 100) * 2 * Math.PI / 180); + if (mainSettingsGUI.flap.val) { + t += (0.02 * mainSettingsGUI.flapSpeed.val); + + var s = t; + + _.each(coords, (coord, name) => { + var u = coordsFlap[coordsFlapIndex][name]; + var v = coordsFlap[(coordsFlapIndex + 1) % 5][name]; + + _.each(['x', 'y', 'z'], dim => { + coords[name][dim] = (u[dim] * (1.0 - s)) + (v[dim] * s); + }); + }); + + if (Math.abs(t - 1.0) <= 0.0001 || t > 1.0) { + coordsFlapIndex++; + coordsFlapIndex %= 5; + t = 0.0; + } } + + renderWing(framework); } -// when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate Framework.init(onLoad, onUpdate); \ No newline at end of file