This example uses ES6 modules to load the three.js library.
Created
July 7, 2020 18:11
-
-
Save mathdoodle/dca3c8deb6ee6cfed75f903f08474609 to your computer and use it in GitHub Desktop.
Getting Started with three.js
This file contains hidden or 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> | |
<base href="/"> | |
<script src="https://unpkg.com/[email protected]/dist/system.js"></script> | |
<link rel='stylesheet' href='style.css'> | |
</head> | |
<body> | |
<div id="three" style="position: absolute; left:0px; top:0px"></div> | |
<script> | |
System.import('./index.js') | |
</script> | |
</body> | |
</html> |
This file contains hidden or 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
import { Scene } from 'three' | |
import { PerspectiveCamera } from 'three' | |
import { WebGLRenderer } from 'three' | |
import { PointLight, AmbientLight } from 'three' | |
import { ImageUtils, RepeatWrapping } from 'three' | |
import { Material } from 'three' | |
import { MeshBasicMaterial, BackSide, DoubleSide } from 'three' | |
import { MeshFaceMaterial } from 'three' | |
import { MeshLambertMaterial } from 'three' | |
import { PlaneGeometry } from 'three' | |
import { Mesh } from 'three' | |
import { AxesHelper } from 'three' | |
import { CubeGeometry, SphereGeometry } from 'three' | |
// | |
// | |
// | |
import { OrbitControls } from './OrbitControls' | |
import { WindowResize } from './WindowResize' | |
let scene: Scene | |
let camera: PerspectiveCamera | |
let renderer: WebGLRenderer | |
let container: HTMLElement | |
let stats: Stats | |
let controls: OrbitControls | |
DomReady.ready(main) | |
function main() { | |
init() | |
animate() | |
} | |
function init() { | |
scene = new Scene() | |
camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20000) | |
scene.add(camera) | |
camera.position.set(0, 150, 400) | |
camera.lookAt(scene.position) | |
renderer = new WebGLRenderer({ antialias: true }) | |
renderer.setSize(window.innerWidth, window.innerHeight) | |
container = document.getElementById('three') as HTMLDivElement | |
container.appendChild(renderer.domElement) | |
WindowResize(renderer, camera) | |
controls = new OrbitControls(camera, renderer.domElement) | |
stats = new Stats() | |
stats.domElement.style.position = 'absolute' | |
stats.domElement.style.bottom = '0px' | |
stats.domElement.style.zIndex = '100' | |
container.appendChild(stats.domElement) | |
const pointLight = new PointLight(0xffffff) | |
pointLight.position.set(0, 250, 0) | |
scene.add(pointLight) | |
const ambientLight = new AmbientLight(0x111111) | |
scene.add(ambientLight) | |
const floorTexture = ImageUtils.loadTexture('assets/img/textures/misc/checkerboard.jpg') | |
floorTexture.wrapS = floorTexture.wrapT = RepeatWrapping | |
floorTexture.repeat.set(10, 10) | |
const floorMaterial = new MeshBasicMaterial({ map: floorTexture, side: DoubleSide }) | |
const floorGeometry = new PlaneGeometry(1000, 1000, 10, 10) | |
const floor = new Mesh(floorGeometry, floorMaterial) | |
floor.position.y = -0.5 | |
floor.rotation.x = Math.PI / 2 | |
scene.add(floor) | |
const skyBoxGeometry = new CubeGeometry(10000, 10000, 10000) | |
const skyBoxMaterial = new MeshBasicMaterial({ color: 0x9999ff, side: BackSide }) | |
const skyBox = new Mesh(skyBoxGeometry, skyBoxMaterial) | |
scene.add(skyBox) | |
const geometry = new SphereGeometry(50, 32, 16) | |
const material = new MeshLambertMaterial({ color: 0x8888ff }) | |
const sphere = new Mesh(geometry, material) | |
sphere.position.set(100, 50, -50) | |
scene.add(sphere) | |
const cubeMaterials: Material[] = [] | |
cubeMaterials.push(new MeshBasicMaterial({ color: 0xff3333 })) | |
cubeMaterials.push(new MeshBasicMaterial({ color: 0xff8800 })) | |
cubeMaterials.push(new MeshBasicMaterial({ color: 0xffff33 })) | |
cubeMaterials.push(new MeshBasicMaterial({ color: 0x33ff33 })) | |
cubeMaterials.push(new MeshBasicMaterial({ color: 0x3333ff })) | |
cubeMaterials.push(new MeshBasicMaterial({ color: 0x8833ff })) | |
const cubeMaterial = new MeshFaceMaterial(cubeMaterials) | |
const cubeGeometry = new CubeGeometry(100, 100, 100, 1, 1, 1) | |
const cube = new Mesh(cubeGeometry, cubeMaterial) | |
cube.position.set(-100, 50, -50) | |
scene.add(cube) | |
const axes = new AxesHelper(100) | |
axes.position.set(0, 50, 0) | |
scene.add(axes) | |
} | |
function animate() { | |
stats.begin() | |
renderer.render(scene, camera) | |
controls.update() | |
stats.end() | |
requestAnimationFrame(animate) | |
} |
This file contains hidden or 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
import { Object3D } from 'three' | |
import { Vector2, Vector3 } from 'three' | |
/** | |
* @author qiao / https://github.com/qiao | |
* @author mrdoob / http://mrdoob.com | |
* @author alteredq / http://alteredqualia.com/ | |
* @author WestLangley / http://github.com/WestLangley | |
* @author David Geo Holmes (TypeScript migration) | |
*/ | |
const EPS = 0.000001 | |
// 65 /*A*/, 83 /*S*/, 68 /*D*/ | |
const keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 } | |
export class OrbitControls { | |
private object: Object3D | |
private domElement: HTMLCanvasElement | Document | |
public enabled = true | |
public center = new Vector3() | |
public userZoom = true | |
public userZoomSpeed = 1.0 | |
public userRotate = true | |
public userRotateSpeed = 1.0 | |
public userPan = true | |
public userPanSpeed = 2.0 | |
public autoRotate = false | |
public autoRotateSpeed = 2.0 // 30 seconds per round when fps is 60 | |
public minPolarAngle = 0 // radians | |
public maxPolarAngle = Math.PI // radians | |
public minDistance = 0 | |
public maxDistance = Infinity | |
private phiDelta = 0 | |
private thetaDelta = 0 | |
private scale = 1 | |
private lastPosition = new Vector3() | |
constructor(object: Object3D, domElement: HTMLCanvasElement) { | |
this.object = object | |
this.domElement = (domElement !== undefined) ? domElement : document | |
const PIXELS_PER_ROUND = 1800 | |
const rotateStart = new Vector2() | |
const rotateEnd = new Vector2() | |
const rotateDelta = new Vector2() | |
const zoomStart = new Vector2() | |
const zoomEnd = new Vector2() | |
const zoomDelta = new Vector2() | |
const STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 } | |
let state = STATE.NONE | |
// events | |
// var changeEvent = { type: 'change' }; | |
this.domElement.addEventListener('contextmenu', function(event: Event) { event.preventDefault() }, false) | |
const onMouseDown = (event: MouseEvent) => { | |
if (this.enabled === false) return | |
if (this.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) | |
} | |
this.domElement.addEventListener('mousedown', onMouseDown, false) | |
const onMouseMove = (event: MouseEvent) => { | |
if (this.enabled === false) return | |
event.preventDefault() | |
if (state === STATE.ROTATE) { | |
rotateEnd.set(event.clientX, event.clientY) | |
rotateDelta.subVectors(rotateEnd, rotateStart) | |
this.rotateLeft(2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * this.userRotateSpeed) | |
this.rotateUp(2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * this.userRotateSpeed) | |
rotateStart.copy(rotateEnd) | |
} | |
else if (state === STATE.ZOOM) { | |
zoomEnd.set(event.clientX, event.clientY) | |
zoomDelta.subVectors(zoomEnd, zoomStart) | |
if (zoomDelta.y > 0) { | |
this.zoomIn() | |
} | |
else { | |
this.zoomOut() | |
} | |
zoomStart.copy(zoomEnd) | |
} else if (state === STATE.PAN) { | |
const movementX: number = event['movementX'] || event['mozMovementX'] || event['webkitMovementX'] || 0 | |
const movementY = event['movementY'] || event['mozMovementY'] || event['webkitMovementY'] || 0 | |
this.pan(new Vector3(- movementX, movementY, 0)) | |
} | |
} | |
const onMouseUp = (_event: MouseEvent) => { | |
if (this.enabled === false) return | |
if (this.userRotate === false) return | |
document.removeEventListener('mousemove', onMouseMove, false) | |
document.removeEventListener('mouseup', onMouseUp, false) | |
state = STATE.NONE | |
} | |
const onMouseWheel = (event: MouseWheelEvent) => { | |
if (this.enabled === false) return | |
if (this.userZoom === false) return | |
let delta = 0 | |
if (event.wheelDelta) { // WebKit / Opera / Explorer 9 | |
delta = event.wheelDelta | |
} | |
else if (event.detail) { // Firefox | |
delta = - event.detail | |
} | |
if (delta > 0) { | |
this.zoomOut() | |
} | |
else { | |
this.zoomIn() | |
} | |
} | |
this.domElement.addEventListener('mousewheel', onMouseWheel, false) | |
this.domElement.addEventListener('DOMMouseScroll', onMouseWheel, false) // firefox | |
const onKeyDown = (event: KeyboardEvent) => { | |
if (this.enabled === false) return | |
if (this.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 keys.ROTATE: | |
state = STATE.ROTATE | |
break | |
case keys.ZOOM: | |
state = STATE.ZOOM | |
break | |
case keys.PAN: | |
state = STATE.PAN | |
break | |
} | |
} | |
const onKeyUp = (event: KeyboardEvent) => { | |
switch (event.keyCode) { | |
case keys.ROTATE: | |
case keys.ZOOM: | |
case keys.PAN: | |
state = STATE.NONE | |
break | |
} | |
} | |
window.addEventListener('keydown', onKeyDown, false) | |
window.addEventListener('keyup', onKeyUp, false) | |
} | |
private getAutoRotationAngle(): number { | |
return 2 * Math.PI / 60 / 60 * this.autoRotateSpeed | |
} | |
private getZoomScale(): number { | |
return Math.pow(0.95, this.userZoomSpeed) | |
} | |
rotateLeft(angle?: number): void { | |
if (angle === undefined) { | |
angle = this.getAutoRotationAngle() | |
} | |
this.thetaDelta -= angle | |
} | |
rotateRight(angle?: number): void { | |
if (angle === undefined) { | |
angle = this.getAutoRotationAngle() | |
} | |
this.thetaDelta += angle | |
} | |
rotateUp(angle?: number) { | |
if (angle === undefined) { | |
angle = this.getAutoRotationAngle() | |
} | |
this.phiDelta -= angle | |
} | |
rotateDown(angle?: number) { | |
if (angle === undefined) { | |
angle = this.getAutoRotationAngle() | |
} | |
this.phiDelta += angle | |
} | |
zoomIn(zoomScale?: number): void { | |
if (zoomScale === undefined) { | |
zoomScale = this.getZoomScale() | |
} | |
this.scale /= zoomScale | |
} | |
zoomOut(zoomScale?: number): void { | |
if (zoomScale === undefined) { | |
zoomScale = this.getZoomScale() | |
} | |
this.scale *= zoomScale | |
} | |
pan(distance: Vector3) { | |
distance.transformDirection(this.object.matrix) | |
distance.multiplyScalar(this.userPanSpeed) | |
this.object.position.add(distance) | |
this.center.add(distance) | |
} | |
update() { | |
const position = this.object.position | |
const offset = position.clone().sub(this.center) | |
// angle from z-axis around y-axis | |
let theta = Math.atan2(offset.x, offset.z) | |
// angle from y-axis | |
let phi = Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y) | |
if (this.autoRotate) { | |
this.rotateLeft(this.getAutoRotationAngle()) | |
} | |
theta += this.thetaDelta | |
phi += this.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)) | |
let radius = offset.length() * this.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) | |
this.thetaDelta = 0 | |
this.phiDelta = 0 | |
this.scale = 1 | |
if (this.lastPosition.distanceTo(this.object.position) > 0) { | |
// this.dispatchEvent( changeEvent ) | |
this.lastPosition.copy(this.object.position) | |
} | |
} | |
} |
This file contains hidden or 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
{ | |
"description": "Getting Started with three.js", | |
"name": "copy-of-three.js with ES6 modules", | |
"version": "1.0.0", | |
"dependencies": { | |
"DomReady": "1.0.0", | |
"stats.js": "0.16.0", | |
"three": "0.117.1" | |
}, | |
"keywords": [ | |
"THREE", | |
"three.js", | |
"stemcstudio", | |
"ES6 modules" | |
], | |
"linting": true, | |
"hideConfigFiles": true, | |
"author": "David Geo Holmes" | |
} |
This file contains hidden or 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
body { | |
background: blue; | |
} |
This file contains hidden or 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
Show hidden characters
{ | |
"allowJs": true, | |
"checkJs": true, | |
"declaration": true, | |
"emitDecoratorMetadata": true, | |
"experimentalDecorators": true, | |
"jsx": "react", | |
"module": "system", | |
"noImplicitAny": true, | |
"noImplicitReturns": true, | |
"noImplicitThis": true, | |
"noUnusedLocals": true, | |
"noUnusedParameters": true, | |
"preserveConstEnums": true, | |
"removeComments": false, | |
"skipLibCheck": true, | |
"sourceMap": true, | |
"strictNullChecks": true, | |
"suppressImplicitAnyIndexErrors": true, | |
"target": "es5", | |
"traceResolution": true | |
} |
This file contains hidden or 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
{ | |
"rules": { | |
"array-type": [ | |
true, | |
"array" | |
], | |
"curly": false, | |
"comment-format": [ | |
true, | |
"check-space" | |
], | |
"eofline": true, | |
"forin": true, | |
"jsdoc-format": true, | |
"new-parens": true, | |
"no-conditional-assignment": false, | |
"no-consecutive-blank-lines": true, | |
"no-construct": true, | |
"no-for-in-array": true, | |
"no-inferrable-types": [ | |
true | |
], | |
"no-magic-numbers": false, | |
"no-shadowed-variable": true, | |
"no-string-throw": true, | |
"no-trailing-whitespace": [ | |
true, | |
"ignore-jsdoc" | |
], | |
"no-var-keyword": true, | |
"one-variable-per-declaration": [ | |
true, | |
"ignore-for-loop" | |
], | |
"prefer-const": true, | |
"prefer-for-of": true, | |
"prefer-function-over-method": false, | |
"prefer-method-signature": true, | |
"radix": true, | |
"semicolon": [ | |
true, | |
"never" | |
], | |
"trailing-comma": [ | |
true, | |
{ | |
"multiline": "never", | |
"singleline": "never" | |
} | |
], | |
"triple-equals": true, | |
"use-isnan": true | |
} | |
} |
This file contains hidden or 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
import { WebGLRenderer } from 'three' | |
import { PerspectiveCamera } from 'three' | |
// This helper makes it easy to handle window resize. | |
// It will update renderer and camera when window is resized. | |
// | |
// # Usage | |
// | |
// **Step 1**: Start updating renderer and camera | |
// | |
// ```var windowResize = WindowResize(aRenderer, aCamera)``` | |
// | |
// **Step 2**: Start updating renderer and camera | |
// | |
// ```windowResize.stop()``` | |
// # Code | |
/** | |
* Update renderer and camera when the window is resized | |
* | |
* @param {Object} renderer the renderer to update | |
* @param {Object} Camera the camera to update | |
*/ | |
export function WindowResize(renderer: WebGLRenderer, camera: PerspectiveCamera) { | |
const callback = function() { | |
// notify the renderer of the size change | |
renderer.setSize(window.innerWidth, window.innerHeight) | |
// update the camera | |
camera.aspect = window.innerWidth / window.innerHeight | |
camera.updateProjectionMatrix() | |
} | |
// bind the resize event | |
window.addEventListener('resize', callback, false) | |
// return .stop() the function to stop watching window resize | |
return { | |
/** | |
* Stop watching window resize | |
*/ | |
stop: function() { | |
window.removeEventListener('resize', callback) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment