Last active
August 20, 2019 12:24
-
-
Save duhaime/4131d0c02ef02257a880eb912767b8dc to your computer and use it in GitHub Desktop.
Low-Poly Scene (Three.js)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js'></script> | |
<script src='OrbitControls.js'></script> | |
<style> | |
html, | |
body { | |
margin: 0; | |
height: 100%; | |
overflow: hidden; | |
background: linear-gradient(#d0e9f1,#9abcc7); | |
} | |
#mountain-target { | |
height: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<div id='mountain-target'></div> | |
<script src='main.js'></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Random number generation | |
**/ | |
Math.seed = Math.initialSeed = window.location.search | |
? parseInt(window.location.search.substring(1).replace('seed=','')) | |
: 219883; | |
Math.randIters = 0; | |
Math.random = function() { | |
Math.randIters++; | |
if (Math.seed == -1) return Math.random(); | |
var max = 1, | |
min = 0; | |
Math.seed = (Math.seed * 9301 + 49297) % 233280; | |
var result = min + (Math.seed/233280) * (max-min); | |
return result; | |
} | |
function World(obj) { | |
this.init(obj); | |
this.land = new Land(this); | |
this.trees = new Trees(this); | |
} | |
World.prototype.init = function(obj) { | |
var obj = obj || {}, | |
container = obj.container || document.querySelector('body'), | |
w = container.clientWidth, | |
h = container.clientHeight; | |
this.scene = new THREE.Scene(); | |
this.camera = new THREE.PerspectiveCamera(75, w/h, 0.001, 10000); | |
this.controls = new THREE.OrbitControls(this.camera, this.container); | |
var renderConfig = {antialias: true, alpha: true}; | |
this.renderer = new THREE.WebGLRenderer(renderConfig); | |
this.renderer.shadowMap.enabled = true; | |
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
this.controls.target = new THREE.Vector3(0, 0, 0.75); | |
this.camera.position.set(130, 85, 130); | |
this.renderer.setPixelRatio(window.devicePixelRatio); | |
this.renderer.setSize(w, h); | |
container.appendChild(this.renderer.domElement); | |
// directional light / sunlight | |
var directional_light = new THREE.DirectionalLight(0xff9900, 0.55); | |
directional_light.position.set(50, 60, -25); | |
directional_light.castShadow = true; | |
directional_light.shadow.camera.near = 10; | |
directional_light.shadow.camera.far = 250; | |
directional_light.shadow.camera.right = 100; | |
directional_light.shadow.camera.left = -100; | |
directional_light.shadow.camera.top = 100; | |
directional_light.shadow.camera.bottom = -100; | |
directional_light.shadow.mapSize.width = 5000; | |
directional_light.shadow.mapSize.height = 5000; | |
this.scene.add(directional_light); | |
// ambient light | |
var ambientLight = new THREE.AmbientLight(0x666666); | |
this.scene.add(ambientLight); | |
// axis helper on demand | |
if (window.location.search.indexOf('axis=true') > -1) { | |
var axesHelper = new THREE.AxesHelper(5000); | |
this.scene.add(axesHelper) | |
} | |
// add resize listener | |
window.addEventListener('resize', function() { | |
this.camera.aspect = container.clientWidth/container.clientHeight; | |
this.camera.updateProjectionMatrix(); | |
this.renderer.setSize(container.clientWidth, container.clientHeight); | |
}.bind(this)) | |
this.render(); | |
} | |
World.prototype.render = function() { | |
requestAnimationFrame(this.render.bind(this)); | |
this.renderer.render(this.scene, this.camera); | |
this.controls.update(); | |
if (this.land && this.land.land) { | |
//this.land.land.rotateOnAxis(new THREE.Vector3(0.0,0.0,0), 1.0); | |
//this.land.land.rotation.y += 0.01; | |
} | |
} | |
/** | |
* Land | |
**/ | |
function Land(world) { | |
this.world = world; | |
this.land = null; // store for the object generated below | |
this.config = { | |
n: 17, // verts per side | |
seed: 0, // seed for rng | |
variation: 6, // degree of experimentation in layout | |
colors: this.getColors(), // colors | |
tileWidth: 10, // how large each square is | |
heightScalar: 50, // how tall the scene is | |
heightExp: 2.4, // higher vals make smoother terrain | |
removeEdges: true, // lop off the edge pieces | |
} | |
this.domain = { // height (y-axis) domain | |
max: Number.NEGATIVE_INFINITY, | |
min: Number.POSITIVE_INFINITY, | |
}; | |
this.render(); | |
} | |
Land.prototype.getColors = function() { | |
return { | |
white0: new THREE.Color(0xFFFFFF), | |
green0: new THREE.Color(0xD6D177), | |
brown0: new THREE.Color(0x614126), | |
brown1: new THREE.Color(0x8E5E39), | |
brown2: new THREE.Color(0x705A3D), | |
} | |
} | |
// return a 2D array with shape n,n where values contain heightmap | |
Land.prototype.getDiamondSquare = function() { | |
function rand(variation) { | |
// return a random number 0:n | |
return (Math.random() * 2 * variation) - variation; | |
} | |
function mean(arr) { | |
// find the mean of 1D array `arr` | |
return arr.reduce(function(sum, i) { | |
return sum + i; | |
}, 0) / arr.length; | |
} | |
function initialize() { | |
// globals | |
var n = this.config.n, | |
variation = this.config.variation, | |
seed = this.config.seed; | |
// create empty 2D array | |
var arr = new Array(this.config.n); | |
for (var i=0; i<this.config.n; i++) { | |
arr[i] = new Array(this.config.n); | |
} | |
// for consistent random number generation on first pass | |
if (Math.randIters < 170) Math.seed = 154708; | |
// initialize corner values | |
arr[0][0] = seed += rand(variation); | |
arr[0][n-1] = seed += rand(variation); | |
arr[n-1][0] = seed += rand(variation); | |
arr[n-1][n-1] = seed += rand(variation); | |
// set the midpoint value | |
var midpoint = parseInt(n/2); | |
arr[midpoint][midpoint] = mean([ | |
arr[0][0], | |
arr[0][n-1], | |
arr[n-1][0], | |
arr[n-1][n-1], | |
]) | |
// modify the values of `arr` to run the diamond star algorithm | |
for (var step=n-1; step>=2; step/=2, variation/=2) { | |
var halfStep = step/2; | |
// square step | |
for (var x=0; x<n-1; x+=step) { | |
for (var y=0; y<n-1; y+=step) { | |
// get each corner | |
var tl = arr[x][y] == undefined ? seed : arr[x][y], | |
tr = arr[x+step] == undefined ? seed : arr[x+step][y], | |
br = arr[x+step] == undefined || arr[x+step][y+step] == undefined ? seed : arr[x+step][y+step], | |
bl = arr[x][y+step] == undefined ? seed : arr[x][y+step]; | |
// smooth and perturb | |
var average = mean([tl, tr, br, bl]) + rand(variation); | |
arr[x+halfStep][y+halfStep] = average; | |
} | |
} | |
// diamond step | |
for (var x=0; x<n; x+=halfStep) { | |
for (var y=(x+halfStep)%step; y<n; y+= step) { | |
// find diamond corners | |
var t = arr[x][(y-halfStep+n-1)%(n-1)], | |
r = arr[(x+halfStep)%(n-1)][y], | |
b = arr[x][(y+halfStep)%(n-1)], | |
l = arr[(x-halfStep+n-1)%(n-1)][y]; | |
// perturb the average | |
var average = mean([t,r,b,l]) + rand(variation); | |
arr[x][y] = average; | |
} | |
} | |
} | |
return arr; | |
} | |
// run the diamond square algorithm | |
arr = initialize.bind(this)(); | |
// lop off all edge pieces | |
if (this.config.removeEdges) { | |
arr = arr.slice(0, this.config.n-2); | |
for (var i=0; i<arr.length; i++) { | |
arr[i] = arr[i].slice(0, this.config.n-2); | |
} | |
} | |
// exponentiate each point and store min and max | |
var max = Number.NEGATIVE_INFINITY, | |
min = Number.POSITIVE_INFINITY; | |
for (var i=0; i<arr.length; i++) { | |
for (var j=0; j<arr[i].length; j++) { | |
arr[i][j] = Math.pow( Math.abs(arr[i][j]), this.config.heightExp); | |
if (arr[i][j] < min) min = arr[i][j]; | |
if (arr[i][j] > max) max = arr[i][j]; | |
} | |
} | |
// scale all points 0:heightScalar | |
var max = max-min, | |
scale = this.config.heightScalar / max, | |
max = max * scale; | |
for (var i=0; i<arr.length; i++) { | |
for (var j=0; j<arr[i].length; j++) { | |
arr[i][j] = (arr[i][j]-min) * scale; | |
} | |
} | |
// smooth out spikes; run lookups on array copy to stop cascading compression | |
var arrCopy = JSON.parse(JSON.stringify(arr)); | |
for (var x=0; x<arr.length; x++) { | |
for (var y=0; y<arr[x].length; y++) { | |
// if any point is twice any neighbor, shrink it | |
var shouldShrink = false; | |
for (var dx=x-1; dx<=x+1; dx++) { | |
for (var dy=y-1; dy<=y+1; dy++) { | |
if (!Array.isArray(arr[dx]) || isNaN(arr[dx][dy]) || (x==dx && y==dy)) { | |
continue; | |
} | |
var delta = arrCopy[x][y] - arrCopy[dx][dy]; | |
if (delta > arrCopy[x][y]/2) shouldShrink = true; | |
} | |
} | |
if (shouldShrink) arr[x][y] *= 0.5; | |
} | |
} | |
// find and store the new max in this.domain | |
for (var x=0; x<arr.length; x++) { | |
for (var y=0; y<arr[x].length; y++) { | |
if (arr[x][y] > this.domain.max) this.domain.max = arr[x][y]; | |
if (arr[x][y] < this.domain.min) this.domain.min = arr[x][y]; | |
} | |
} | |
// if ~3% of squares aren't half the max required height, recompute | |
var keepers = 0; | |
for (var x=0; x<arr.length; x++) { | |
for (var y=0; y<arr[x].length; y++) { | |
if (arr[x][y] > this.config.heightScalar/2) { | |
keepers++; | |
} | |
} | |
} | |
// either return the computed points or recompute if necessary | |
var keepersRequired = (arr.length*arr[0].length) * 0.026666; | |
return keepers > keepersRequired | |
? arr | |
: this.getDiamondSquare(); | |
} | |
Land.prototype.render = function() { | |
var arr = this.getDiamondSquare(), | |
land = new THREE.Object3D(); | |
land.name = 'land'; | |
for (var x=0; x<arr.length-1; x++) { | |
for (var y=0; y<arr[x].length-1; y++) { | |
var geometry = new THREE.Geometry(), | |
xWest = this.config.tileWidth * x, | |
xEast = this.config.tileWidth + xWest, | |
zNorth = this.config.tileWidth * y, | |
zSouth = this.config.tileWidth + zNorth, | |
yNorthWest = arr[x][y], | |
ySouthWest = arr[x][y+1], | |
yNorthEast = arr[x+1][y], | |
ySouthEast = arr[x+1][y+1]; | |
geometry.vertices.push(new THREE.Vector3(xWest, yNorthWest, zNorth)); // nw | |
geometry.vertices.push(new THREE.Vector3(xWest, ySouthWest, zSouth)); // sw | |
geometry.vertices.push(new THREE.Vector3(xEast, yNorthEast, zNorth)); // ne | |
geometry.vertices.push(new THREE.Vector3(xEast, ySouthEast, zSouth)); // se | |
geometry.faces.push(new THREE.Face3(1, 2, 0)); // sw, ne, nw | |
geometry.faces.push(new THREE.Face3(1, 3, 2)); // sw, se, ne | |
geometry.faceVertexUvs[0].push([ | |
new THREE.Vector2(0, 0), | |
new THREE.Vector2(0, 1), | |
new THREE.Vector2(1, 0), | |
]); | |
geometry.faceVertexUvs[0].push([ | |
new THREE.Vector2(0, 0), | |
new THREE.Vector2(0, 1), | |
new THREE.Vector2(1, 1), | |
]); | |
geometry.computeFaceNormals(); | |
geometry.computeVertexNormals(); | |
// build the material | |
var material = new THREE.MeshLambertMaterial({ | |
flatShading: true, | |
vertexColors: THREE.FaceColors, | |
side: THREE.DoubleSide, | |
}); | |
// add color to each face | |
var height0 = Math.max(ySouthWest, yNorthEast, yNorthWest), | |
height1 = Math.max(ySouthWest, ySouthEast, yNorthEast); | |
var color0 = this.getColorAtHeight(height0); | |
var color1 = this.getColorAtHeight(height1); | |
geometry.faces[0].color = color0; | |
geometry.faces[1].color = color1; | |
// add the mesh to the group | |
var mesh = new THREE.Mesh(geometry, material); | |
mesh.receiveShadow = true; | |
land.add(mesh); | |
} | |
} | |
// store references to the lengths of each side | |
this.xLen = (arr.length-1) * this.config.tileWidth; | |
this.zLen = (arr[0].length-1) * this.config.tileWidth; | |
this.land = land; | |
this.addSides(arr); | |
this.addBase(); | |
// translate the land to center it | |
this.land.translateX(-this.xLen/2); | |
this.land.translateZ(-this.zLen/2); | |
this.land.updateMatrixWorld(); | |
// add the land to the scene | |
this.world.scene.add(this.land); | |
} | |
Land.prototype.addSides = function(arr) { | |
// add the the left x axis base | |
var x0 = this.getSide(arr[0], THREE.FrontSide); | |
x0.name = 'x0'; | |
x0.rotation.y = 270 * Math.PI/180; | |
this.land.add(x0); | |
// add the right x axis base -- near right on page load | |
var arrX1 = arr[arr.length-1].slice().reverse(), | |
x1 = this.getSide(arrX1, THREE.FrontSide); | |
x1.name = 'x1'; | |
x1.rotation.y = 90 * Math.PI/180; | |
x1.position.x += this.xLen; | |
x1.position.z += this.zLen; | |
this.land.add(x1); | |
// add the left z axis base -- far right on page load | |
var arrZ0 = [], | |
arrZ1 = []; | |
for (var i=0; i<arr.length; i++) { | |
arrZ0.push(arr[i][0]); | |
arrZ1.push(arr[i][arr[i].length-1]); | |
} | |
var z0 = this.getSide(arrZ0, THREE.BackSide); | |
z0.name = 'z0'; | |
this.land.add(z0); | |
// add the left z axis base -- near left on page load | |
var z1 = this.getSide(arrZ1, THREE.FrontSide); | |
z1.name = 'z1'; | |
z1.rotation.y = 0 * Math.PI/180; | |
z1.position.z += this.zLen; | |
this.land.add(z1); | |
} | |
Land.prototype.getSide = function(arr, sideType) { | |
var side = new THREE.Object3D(), | |
lightMaterial = new THREE.MeshLambertMaterial({ | |
color: this.config.colors.brown1, | |
flatShading: true, | |
side: sideType, | |
}), | |
darkMaterial = new THREE.MeshLambertMaterial({ | |
color: this.config.colors.brown0, | |
flatShading: true, | |
side: sideType, | |
}); | |
for (var i=0; i<arr.length-1; i++) { | |
// top soil | |
var xLeft = this.config.tileWidth*i, | |
xRight = xLeft + this.config.tileWidth, | |
yTopLeft = arr[i], | |
yTopRight = arr[i+1], | |
yMiddleLeft = (yTopLeft - this.domain.max)/3, | |
yMiddleRight = (yTopRight - this.domain.max)/3; | |
var geometry = new THREE.Geometry(); | |
geometry.vertices.push(new THREE.Vector3(xLeft, yMiddleLeft, 0)); | |
geometry.vertices.push(new THREE.Vector3(xRight, yTopRight, 0)); | |
geometry.vertices.push(new THREE.Vector3(xLeft, yTopLeft, 0)); | |
geometry.vertices.push(new THREE.Vector3(xRight, yMiddleRight, 0)); | |
geometry.faces.push(new THREE.Face3(0,1,2)); | |
geometry.faces.push(new THREE.Face3(0,3,1)); | |
var mesh = new THREE.Mesh(geometry, lightMaterial); | |
side.add(mesh); | |
// bottom soil | |
var yBottom = -this.domain.max/2, | |
geometry = new THREE.Geometry(); | |
geometry.vertices.push(new THREE.Vector3(xLeft, yBottom, 0)); | |
geometry.vertices.push(new THREE.Vector3(xRight, yMiddleRight, 0)); | |
geometry.vertices.push(new THREE.Vector3(xLeft, yMiddleLeft, 0)); | |
geometry.vertices.push(new THREE.Vector3(xRight, yBottom, 0)); | |
geometry.faces.push(new THREE.Face3(0,1,2)); | |
geometry.faces.push(new THREE.Face3(0,3,1)); | |
geometry.computeFaceNormals(); | |
var mesh = new THREE.Mesh(geometry, darkMaterial) | |
side.add(mesh); | |
} | |
return side; | |
} | |
Land.prototype.addBase = function() { | |
var material = new THREE.MeshLambertMaterial({ | |
color: 0x614126, | |
shading: THREE.FlatShading, | |
side: THREE.DoubleSide, | |
}) | |
var geometry = new THREE.PlaneBufferGeometry(this.xLen, this.zLen, 1); | |
var mesh = new THREE.Mesh(geometry, material); | |
mesh.position.y = -this.domain.max/2; | |
mesh.position.x = this.xLen/2; | |
mesh.position.z = this.zLen/2; | |
mesh.rotation.x = Math.PI/2; | |
mesh.name = 'base'; | |
this.land.add(mesh); | |
} | |
Land.prototype.getColorAtHeight = function(height) { | |
if (height/this.domain.max >= 0.8) return this.config.colors.white0; | |
if (height/this.domain.max >= 0.49) return this.config.colors.brown0; | |
return this.config.colors.green0; | |
} | |
Land.prototype.getRandomSurfaceCoords = function() { | |
var min = -this.xLen/2, | |
max = this.xLen/2, | |
x = min + (Math.random() * (max-min)), | |
z = min + (Math.random() * (max-min)); | |
return { | |
x: x, | |
y: this.getHeightAt({x: x, z: z}), | |
z: z, | |
} | |
} | |
Land.prototype.getHeightAt = function(pos) { | |
pos.y = 100; // random height above terrain | |
var raycaster = new THREE.Raycaster(); | |
raycaster.set(pos, new THREE.Vector3(0, -1, 0)); | |
var intersects = raycaster.intersectObject(this.land, true); | |
return intersects.length | |
? intersects[0].point.y | |
: null; | |
} | |
/** | |
* Trees | |
**/ | |
function Trees(world) { | |
this.world = world; | |
this.init(); | |
} | |
Trees.prototype.init = function() { | |
this.config = { | |
height: 4.5, | |
n: parseInt(Math.random() * 100), | |
} | |
var trees = new THREE.Object3D(), | |
material = new THREE.MeshLambertMaterial({ | |
color: 0x9ACD32, | |
flatShading: true, | |
}); | |
for (var i=0; i<this.config.n; i++) { | |
var h = this.config.height + Math.random() - Math.random(), | |
r = h/5, | |
geometry = new THREE.Geometry(); | |
geometry.vertices.push( new THREE.Vector3( 0, h, 0 ) ); // top | |
geometry.vertices.push( new THREE.Vector3( -r, 0, r ) ); // sw | |
geometry.vertices.push( new THREE.Vector3( r, 0, r ) ); // se | |
geometry.vertices.push( new THREE.Vector3( r, 0, -r ) ); // ne | |
geometry.vertices.push( new THREE.Vector3( -r, 0, -r ) ); // nw | |
geometry.faces.push( new THREE.Face3( 1, 2, 0 ) ); // south | |
geometry.faces.push( new THREE.Face3( 2, 3, 0 ) ); // east | |
geometry.faces.push( new THREE.Face3( 3, 4, 0 ) ); // north | |
geometry.faces.push( new THREE.Face3( 4, 1, 0 ) ); // west | |
geometry.faces.push( new THREE.Face3( 3, 2, 1 ) ); // base, upside down, remember | |
geometry.faces.push( new THREE.Face3( 4, 3, 1 ) ); // base | |
geometry.computeFaceNormals(); | |
var tree = new THREE.Mesh(geometry, material), | |
pos = this.world.land.getRandomSurfaceCoords(), | |
halfLen = this.world.land.xLen/2; | |
if (!pos.x || !pos.y || !pos.z || | |
(Math.abs(pos.x) + 2*r) > halfLen || | |
(Math.abs(pos.z) + 2*r) > halfLen || | |
pos.y > 30) continue; // limit tree altitude | |
tree.position.set(pos.x, pos.y, pos.z); | |
tree.rotation.y = Math.PI * Math.random() * 4; | |
tree.name = 'tree'; | |
tree.castShadow = true; | |
trees.add(tree) | |
} | |
trees.name = 'trees'; | |
this.world.scene.add(trees); | |
} | |
/** | |
* Main | |
**/ | |
var world = new World({ | |
container: document.querySelector('#mountain-target'), | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @author qiao / https://github.com/qiao | |
* @author mrdoob / http://mrdoob.com | |
* @author alteredq / http://alteredqualia.com/ | |
* @author WestLangley / http://github.com/WestLangley | |
*/ | |
THREE.OrbitControls = function ( object, domElement ) { | |
this.object = object; | |
this.domElement = ( domElement !== undefined ) ? domElement : document; | |
// API | |
this.enabled = true; | |
this.center = new THREE.Vector3(); | |
this.userZoom = true; | |
this.userZoomSpeed = 1.0; | |
this.userRotate = true; | |
this.userRotateSpeed = 1.0; | |
this.userPan = true; | |
this.userPanSpeed = 2.0; | |
this.autoRotate = false; | |
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 | |
this.minPolarAngle = 0; // radians | |
this.maxPolarAngle = Math.PI; // radians | |
this.minDistance = 0; | |
this.maxDistance = Infinity; | |
// 65 /*A*/, 83 /*S*/, 68 /*D*/ | |
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 }; | |
// internals | |
var scope = this; | |
var EPS = 0.000001; | |
var PIXELS_PER_ROUND = 1800; | |
var rotateStart = new THREE.Vector2(); | |
var rotateEnd = new THREE.Vector2(); | |
var rotateDelta = new THREE.Vector2(); | |
var zoomStart = new THREE.Vector2(); | |
var zoomEnd = new THREE.Vector2(); | |
var zoomDelta = new THREE.Vector2(); | |
var phiDelta = 0; | |
var thetaDelta = 0; | |
var scale = 1; | |
var lastPosition = new THREE.Vector3(); | |
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 }; | |
var state = STATE.NONE; | |
// events | |
var changeEvent = { type: 'change' }; | |
this.rotateLeft = function ( angle ) { | |
if ( angle === undefined ) { | |
angle = getAutoRotationAngle(); | |
} | |
thetaDelta -= angle; | |
}; | |
this.rotateRight = function ( angle ) { | |
if ( angle === undefined ) { | |
angle = getAutoRotationAngle(); | |
} | |
thetaDelta += angle; | |
}; | |
this.rotateUp = function ( angle ) { | |
if ( angle === undefined ) { | |
angle = getAutoRotationAngle(); | |
} | |
phiDelta -= angle; | |
}; | |
this.rotateDown = function ( angle ) { | |
if ( angle === undefined ) { | |
angle = getAutoRotationAngle(); | |
} | |
phiDelta += angle; | |
}; | |
this.zoomIn = function ( zoomScale ) { | |
if ( zoomScale === undefined ) { | |
zoomScale = getZoomScale(); | |
} | |
scale /= zoomScale; | |
}; | |
this.zoomOut = function ( zoomScale ) { | |
if ( zoomScale === undefined ) { | |
zoomScale = getZoomScale(); | |
} | |
scale *= zoomScale; | |
}; | |
this.pan = function ( distance ) { | |
distance.transformDirection( this.object.matrix ); | |
distance.multiplyScalar( scope.userPanSpeed ); | |
this.object.position.add( distance ); | |
this.center.add( distance ); | |
}; | |
this.update = function () { | |
var position = this.object.position; | |
var offset = position.clone().sub( this.center ); | |
// angle from z-axis around y-axis | |
var theta = Math.atan2( offset.x, offset.z ); | |
// angle from y-axis | |
var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); | |
if ( this.autoRotate ) { | |
this.rotateLeft( getAutoRotationAngle() ); | |
} | |
theta += thetaDelta; | |
phi += phiDelta; | |
// restrict phi to be between desired limits | |
phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); | |
// restrict phi to be betwee EPS and PI-EPS | |
phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); | |
var radius = offset.length() * scale; | |
// restrict radius to be between desired limits | |
radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); | |
offset.x = radius * Math.sin( phi ) * Math.sin( theta ); | |
offset.y = radius * Math.cos( phi ); | |
offset.z = radius * Math.sin( phi ) * Math.cos( theta ); | |
position.copy( this.center ).add( offset ); | |
this.object.lookAt( this.center ); | |
thetaDelta = 0; | |
phiDelta = 0; | |
scale = 1; | |
if ( lastPosition.distanceTo( this.object.position ) > 0 ) { | |
this.dispatchEvent( changeEvent ); | |
lastPosition.copy( this.object.position ); | |
} | |
}; | |
function getAutoRotationAngle() { | |
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; | |
} | |
function getZoomScale() { | |
return Math.pow( 0.95, scope.userZoomSpeed ); | |
} | |
function onMouseDown( event ) { | |
if ( scope.enabled === false ) return; | |
if ( scope.userRotate === false ) return; | |
event.preventDefault(); | |
if ( state === STATE.NONE ) | |
{ | |
if ( event.button === 0 ) | |
state = STATE.ROTATE; | |
if ( event.button === 1 ) | |
state = STATE.ZOOM; | |
if ( event.button === 2 ) | |
state = STATE.PAN; | |
} | |
if ( state === STATE.ROTATE ) { | |
//state = STATE.ROTATE; | |
rotateStart.set( event.clientX, event.clientY ); | |
} else if ( state === STATE.ZOOM ) { | |
//state = STATE.ZOOM; | |
zoomStart.set( event.clientX, event.clientY ); | |
} else if ( state === STATE.PAN ) { | |
//state = STATE.PAN; | |
} | |
document.addEventListener( 'mousemove', onMouseMove, false ); | |
document.addEventListener( 'mouseup', onMouseUp, false ); | |
} | |
function onMouseMove( event ) { | |
if ( scope.enabled === false ) return; | |
event.preventDefault(); | |
if ( state === STATE.ROTATE ) { | |
rotateEnd.set( event.clientX, event.clientY ); | |
rotateDelta.subVectors( rotateEnd, rotateStart ); | |
scope.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * scope.userRotateSpeed ); | |
scope.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * scope.userRotateSpeed ); | |
rotateStart.copy( rotateEnd ); | |
} else if ( state === STATE.ZOOM ) { | |
zoomEnd.set( event.clientX, event.clientY ); | |
zoomDelta.subVectors( zoomEnd, zoomStart ); | |
if ( zoomDelta.y > 0 ) { | |
scope.zoomIn(); | |
} else { | |
scope.zoomOut(); | |
} | |
zoomStart.copy( zoomEnd ); | |
} else if ( state === STATE.PAN ) { | |
var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; | |
var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; | |
scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) ); | |
} | |
} | |
function onMouseUp( event ) { | |
if ( scope.enabled === false ) return; | |
if ( scope.userRotate === false ) return; | |
document.removeEventListener( 'mousemove', onMouseMove, false ); | |
document.removeEventListener( 'mouseup', onMouseUp, false ); | |
state = STATE.NONE; | |
} | |
function onMouseWheel( event ) { | |
if ( scope.enabled === false ) return; | |
if ( scope.userZoom === false ) return; | |
var delta = 0; | |
if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 | |
delta = event.wheelDelta; | |
} else if ( event.detail ) { // Firefox | |
delta = - event.detail; | |
} | |
if ( delta > 0 ) { | |
scope.zoomOut(); | |
} else { | |
scope.zoomIn(); | |
} | |
} | |
function onKeyDown( event ) { | |
if ( scope.enabled === false ) return; | |
if ( scope.userPan === false ) return; | |
switch ( event.keyCode ) { | |
/*case scope.keys.UP: | |
scope.pan( new THREE.Vector3( 0, 1, 0 ) ); | |
break; | |
case scope.keys.BOTTOM: | |
scope.pan( new THREE.Vector3( 0, - 1, 0 ) ); | |
break; | |
case scope.keys.LEFT: | |
scope.pan( new THREE.Vector3( - 1, 0, 0 ) ); | |
break; | |
case scope.keys.RIGHT: | |
scope.pan( new THREE.Vector3( 1, 0, 0 ) ); | |
break; | |
*/ | |
case scope.keys.ROTATE: | |
state = STATE.ROTATE; | |
break; | |
case scope.keys.ZOOM: | |
state = STATE.ZOOM; | |
break; | |
case scope.keys.PAN: | |
state = STATE.PAN; | |
break; | |
} | |
} | |
function onKeyUp( event ) { | |
switch ( event.keyCode ) { | |
case scope.keys.ROTATE: | |
case scope.keys.ZOOM: | |
case scope.keys.PAN: | |
state = STATE.NONE; | |
break; | |
} | |
} | |
this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); | |
this.domElement.addEventListener( 'mousedown', onMouseDown, false ); | |
this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); | |
this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox | |
window.addEventListener( 'keydown', onKeyDown, false ); | |
window.addEventListener( 'keyup', onKeyUp, false ); | |
}; | |
THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment