使用 three.js,您不再需要花哨的游戏PC或控制台来显示逼真的3D图形。 您甚至不需要下载特殊的应用程序。现在每个人都可以使用智能手机和网络浏览器体验令人惊叹的3D应用程序。
这个惊人的库和充满活力的社区是您在浏览器、笔记本电脑、平板电脑或智能手机上创建游戏、 音乐视频、科学和数据可视化或几乎任何您能想象的任何东西所需要的一切!
可在任何操作系统和设备(从智能手机到笔记本电脑到智能手表) 上运行的令人惊叹的、专业品质的、高性能的3D Web应用程序所需的一切, 即使您对Web开发和计算机图形完全陌生。three.js是有史以来最易于访问的计算机图形框架, 我们将充分利用它来引导您很快的获得高水平的专业知识。了解基本的three.js应用程序所需的所有基本概念,有了这些知识, 您将可以立即创建自己的惊人项目。有了三维的力量,唯一的限制就是你的想象力!
引入three.js和cannon.js依赖
html
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/109/three.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
前置知识点
JavaScript的原型和原型链实现类似强类型语言的继承功能
javascript
function EventDispatcher() {}
function Object3D() {}
Object3D.prototype = Object.assign(
Object.create( EventDispatcher.prototype ),
{
constructor: Object3D
}
);
three.js大量使用了上面这种继承方式,其主要原理是通过Object.create创建一个对象,对象的__proto__属性指向EventDispatcher的原型链。
javascript
Object.create(EventDispatcher.prototype )
然后通过 Object.assign方法把扩展属性进行合并;注意这里的constructor,这里把Object3D的构造函数也合并起来,放在Object3D.prototype的原型链上。不然在Object3D.prototype赋值的时候,构造函数就会找不到。
在three.js
中使用的是右手坐标系,原因是WebGL
默认的就是这种坐标系。简单理解就是,x轴正方向向右,y轴正方向向上,z轴正方向由屏幕从里向外。在场景中所有的物体在容器的位置都是依靠这个坐标系设置的。
一般使用的坐标系是当你面朝计算机屏幕时, X轴
是水平的(正方向为右), Y轴
是垂直的(正方向为上), Z轴
垂直于屏幕(正方向为外),这个坐标系也被称为 右手坐标系
。之所被称为 右手坐标系
是因为它是通过如图所示的 右手手势
确定的,即当你伸出右手摆出如图所示手势时, 拇指
指向 X轴的正方向
, 食指
指向 Y轴的正方向
, 中指
指向 Z轴的正方向
,这种确定坐标系方式也被称为 右手定则
。
场景scene ,它就相当于一个大容器,我们需要展示的所有物体都要放入场景。它又被称为场景图,因为它是一个树形结构数据。能放入场景中的对象都继承了Object3D对象,所以每一个子节点都有自己的局部空间。简单理解就是场景中有一个空间可以添加组、Object3D、网格等物体类型,子节点也是一个小容器,同样可以添加组、Object3D、网格等物体类型。区别就是,子节点设置的坐标位置是相对于父节点的局部空间坐标来改变的。
建立几何体时通过指定几何体的顶点和三角形的面确定了几何体的形状,另外还需要给几何体新增皮肤才能实现物体的效果,材质就像物体的皮肤,决定了物体的质感。
基础材质:以简单着色方式来绘制几何体的材质,不受光照影响。
深度材质:按深度绘制几何体的材质。深度基于相机远近端面,离近端面越近就越白,离远端面越近就越黑。
法向量材质:把法向量对映到RGB颜色的材质。
Lambert材质:是一种需要光源的材质,非光泽表面的材质,没有镜面高光,适用于石膏等表面粗糙的物体。
Phong材质:也是一种需要光源的材质,具有镜面高光的光泽表面的材质,适用于金属、漆面等反光的物体。
材质捕获:使用储存了光照和反射等信息的贴图,然后利用法线方向进行取样。优点是可以用很低的消耗来实现很多特殊风格的效果;缺点是仅对于固定相机视角的情况较好。
下图是使用不同贴图实现的效果:
透视相机近大远小,同样大小的物体离相机近的在画面上显得大,离相机远的物体在画面上显得小。透视相机的视锥体如上图左侧所示,从近端面到远端面构成的区域内的物体才能显示在影象上。
正交相机无论物体距离相机远或者近,在最终渲染的图片中物体的大小都保持不变。正交相机的视锥体如下图右侧所示,和透视相机一样,从近端面到远端面构成的区域内的物体才能显示在影象上。
立即执行函数(function(){})(),先了解些函数的基本概念(函数声明、函数表达式、匿名函数)。加运算符确实可将函数声明转化为函数表达式,而之所以使用括号,是因为括号相对其他运算符会更安全。
在three.js基础上增加控制器
javascript
( function () {
'use strict';
var GizmoMaterial = function ( parameters ) {
THREE.MeshBasicMaterial.call( this );
this.depthTest = false;
this.depthWrite = false;
this.side = THREE.FrontSide;
this.transparent = true;
this.setValues( parameters );
this.oldColor = this.color.clone();
this.oldOpacity = this.opacity;
this.highlight = function( highlighted ) {
if ( highlighted ) {
this.color.setRGB( 1, 1, 0 );
this.opacity = 1;
} else {
this.color.copy( this.oldColor );
this.opacity = this.oldOpacity;
}
};
};
GizmoMaterial.prototype = Object.create( THREE.MeshBasicMaterial.prototype );
GizmoMaterial.prototype.constructor = GizmoMaterial;
var GizmoLineMaterial = function ( parameters ) {
THREE.LineBasicMaterial.call( this );
this.depthTest = false;
this.depthWrite = false;
this.transparent = true;
this.linewidth = 1;
this.setValues( parameters );
this.oldColor = this.color.clone();
this.oldOpacity = this.opacity;
this.highlight = function( highlighted ) {
if ( highlighted ) {
this.color.setRGB( 1, 1, 0 );
this.opacity = 1;
} else {
this.color.copy( this.oldColor );
this.opacity = this.oldOpacity;
}
};
};
GizmoLineMaterial.prototype = Object.create( THREE.LineBasicMaterial.prototype );
GizmoLineMaterial.prototype.constructor = GizmoLineMaterial;
var pickerMaterial = new GizmoMaterial( { visible: false, transparent: false } );
THREE.TransformGizmo = function () {
var scope = this;
this.init = function () {
THREE.Object3D.call( this );
this.handles = new THREE.Object3D();
this.pickers = new THREE.Object3D();
this.planes = new THREE.Object3D();
this.add( this.handles );
this.add( this.pickers );
this.add( this.planes );
PLANES
var planeGeometry = new THREE.PlaneBufferGeometry( 50, 50, 2, 2 );
var planeMaterial = new THREE.MeshBasicMaterial( { visible: false, side: THREE.DoubleSide } );
var planes = {
"XY": new THREE.Mesh( planeGeometry, planeMaterial ),
"YZ": new THREE.Mesh( planeGeometry, planeMaterial ),
"XZ": new THREE.Mesh( planeGeometry, planeMaterial ),
"XYZE": new THREE.Mesh( planeGeometry, planeMaterial )
};
this.activePlane = planes[ "XYZE" ];
planes[ "YZ" ].rotation.set( 0, Math.PI / 2, 0 );
planes[ "XZ" ].rotation.set( - Math.PI / 2, 0, 0 );
for ( var i in planes ) {
planes[ i ].name = i;
this.planes.add( planes[ i ] );
this.planes[ i ] = planes[ i ];
}
HANDLES AND PICKERS
var setupGizmos = function( gizmoMap, parent ) {
for ( var name in gizmoMap ) {
for ( i = gizmoMap[ name ].length; i --; ) {
var object = gizmoMap[ name ][ i ][ 0 ];
var position = gizmoMap[ name ][ i ][ 1 ];
var rotation = gizmoMap[ name ][ i ][ 2 ];
object.name = name;
if ( position ) object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] );
if ( rotation ) object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] );
parent.add( object );
}
}
};
setupGizmos( this.handleGizmos, this.handles );
setupGizmos( this.pickerGizmos, this.pickers );
// reset Transformations
this.traverse( function ( child ) {
if ( child instanceof THREE.Mesh ) {
child.updateMatrix();
var tempGeometry = child.geometry.clone();
tempGeometry.applyMatrix( child.matrix );
child.geometry = tempGeometry;
child.position.set( 0, 0, 0 );
child.rotation.set( 0, 0, 0 );
child.scale.set( 1, 1, 1 );
}
} );
};
this.highlight = function ( axis ) {
this.traverse( function( child ) {
if ( child.material && child.material.highlight ) {
if ( child.name === axis ) {
child.material.highlight( true );
} else {
child.material.highlight( false );
}
}
} );
};
};
THREE.TransformGizmo.prototype = Object.create( THREE.Object3D.prototype );
THREE.TransformGizmo.prototype.constructor = THREE.TransformGizmo;
THREE.TransformGizmo.prototype.update = function ( rotation, eye ) {
var vec1 = new THREE.Vector3( 0, 0, 0 );
var vec2 = new THREE.Vector3( 0, 1, 0 );
var lookAtMatrix = new THREE.Matrix4();
this.traverse( function( child ) {
if ( child.name.search( "E" ) !== - 1 ) {
child.quaternion.setFromRotationMatrix( lookAtMatrix.lookAt( eye, vec1, vec2 ) );
} else if ( child.name.search( "X" ) !== - 1 || child.name.search( "Y" ) !== - 1 || child.name.search( "Z" ) !== - 1 ) {
child.quaternion.setFromEuler( rotation );
}
} );
};
THREE.TransformGizmoTranslate = function () {
THREE.TransformGizmo.call( this );
var arrowGeometry = new THREE.Geometry();
var mesh = new THREE.Mesh( new THREE.CylinderGeometry( 0, 0.05, 0.2, 12, 1, false ) );
mesh.position.y = 0.5;
mesh.updateMatrix();
arrowGeometry.merge( mesh.geometry, mesh.matrix );
var lineXGeometry = new THREE.BufferGeometry();
lineXGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) );
var lineYGeometry = new THREE.BufferGeometry();
lineYGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );
var lineZGeometry = new THREE.BufferGeometry();
lineZGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );
this.handleGizmos = {
X: [
[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0xff0000 } ) ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ],
[ new THREE.Line( lineXGeometry, new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
],
Y: [
[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x00ff00 } ) ), [ 0, 0.5, 0 ] ],
[ new THREE.Line( lineYGeometry, new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
],
Z: [
[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x0000ff } ) ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ] ],
[ new THREE.Line( lineZGeometry, new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
],
XYZ: [
[ new THREE.Mesh( new THREE.OctahedronGeometry( 0.1, 0 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ), [ 0, 0, 0 ], [ 0, 0, 0 ] ]
],
XY: [
[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0xffff00, opacity: 0.25 } ) ), [ 0.15, 0.15, 0 ] ]
],
YZ: [
[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0x00ffff, opacity: 0.25 } ) ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ] ]
],
XZ: [
[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0xff00ff, opacity: 0.25 } ) ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ] ]
]
};
this.pickerGizmos = {
X: [
[ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
],
Y: [
[ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0.6, 0 ] ]
],
Z: [
[ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ] ]
],
XYZ: [
[ new THREE.Mesh( new THREE.OctahedronGeometry( 0.2, 0 ), pickerMaterial ) ]
],
XY: [
[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0.2, 0.2, 0 ] ]
],
YZ: [
[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0, 0.2, 0.2 ], [ 0, Math.PI / 2, 0 ] ]
],
XZ: [
[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0.2, 0, 0.2 ], [ - Math.PI / 2, 0, 0 ] ]
]
};
this.setActivePlane = function ( axis, eye ) {
var tempMatrix = new THREE.Matrix4();
eye.applyMatrix4( tempMatrix.getInverse( tempMatrix.extractRotation( this.planes[ "XY" ].matrixWorld ) ) );
if ( axis === "X" ) {
this.activePlane = this.planes[ "XY" ];
if ( Math.abs( eye.y ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "XZ" ];
}
if ( axis === "Y" ) {
this.activePlane = this.planes[ "XY" ];
if ( Math.abs( eye.x ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "YZ" ];
}
if ( axis === "Z" ) {
this.activePlane = this.planes[ "XZ" ];
if ( Math.abs( eye.x ) > Math.abs( eye.y ) ) this.activePlane = this.planes[ "YZ" ];
}
if ( axis === "XYZ" ) this.activePlane = this.planes[ "XYZE" ];
if ( axis === "XY" ) this.activePlane = this.planes[ "XY" ];
if ( axis === "YZ" ) this.activePlane = this.planes[ "YZ" ];
if ( axis === "XZ" ) this.activePlane = this.planes[ "XZ" ];
};
this.init();
};
THREE.TransformGizmoTranslate.prototype = Object.create( THREE.TransformGizmo.prototype );
THREE.TransformGizmoTranslate.prototype.constructor = THREE.TransformGizmoTranslate;
THREE.TransformGizmoRotate = function () {
THREE.TransformGizmo.call( this );
var CircleGeometry = function ( radius, facing, arc ) {
var geometry = new THREE.BufferGeometry();
var vertices = [];
arc = arc ? arc : 1;
for ( var i = 0; i <= 64 * arc; ++ i ) {
if ( facing === 'x' ) vertices.push( 0, Math.cos( i / 32 * Math.PI ) * radius, Math.sin( i / 32 * Math.PI ) * radius );
if ( facing === 'y' ) vertices.push( Math.cos( i / 32 * Math.PI ) * radius, 0, Math.sin( i / 32 * Math.PI ) * radius );
if ( facing === 'z' ) vertices.push( Math.sin( i / 32 * Math.PI ) * radius, Math.cos( i / 32 * Math.PI ) * radius, 0 );
}
geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
return geometry;
};
this.handleGizmos = {
X: [
[ new THREE.Line( new CircleGeometry( 1, 'x', 0.5 ), new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
],
Y: [
[ new THREE.Line( new CircleGeometry( 1, 'y', 0.5 ), new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
],
Z: [
[ new THREE.Line( new CircleGeometry( 1, 'z', 0.5 ), new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
],
E: [
[ new THREE.Line( new CircleGeometry( 1.25, 'z', 1 ), new GizmoLineMaterial( { color: 0xcccc00 } ) ) ]
],
XYZE: [
[ new THREE.Line( new CircleGeometry( 1, 'z', 1 ), new GizmoLineMaterial( { color: 0x787878 } ) ) ]
]
};
this.pickerGizmos = {
X: [
[ new THREE.Mesh( new THREE.TorusGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ] ]
],
Y: [
[ new THREE.Mesh( new THREE.TorusGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ] ]
],
Z: [
[ new THREE.Mesh( new THREE.TorusGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
],
E: [
[ new THREE.Mesh( new THREE.TorusGeometry( 1.25, 0.12, 2, 24 ), pickerMaterial ) ]
],
XYZE: [
[ new THREE.Mesh( new THREE.Geometry() ) ]// TODO
]
};
this.setActivePlane = function ( axis ) {
if ( axis === "E" ) this.activePlane = this.planes[ "XYZE" ];
if ( axis === "X" ) this.activePlane = this.planes[ "YZ" ];
if ( axis === "Y" ) this.activePlane = this.planes[ "XZ" ];
if ( axis === "Z" ) this.activePlane = this.planes[ "XY" ];
};
this.update = function ( rotation, eye2 ) {
THREE.TransformGizmo.prototype.update.apply( this, arguments );
var group = {
handles: this[ "handles" ],
pickers: this[ "pickers" ],
};
var tempMatrix = new THREE.Matrix4();
var worldRotation = new THREE.Euler( 0, 0, 1 );
var tempQuaternion = new THREE.Quaternion();
var unitX = new THREE.Vector3( 1, 0, 0 );
var unitY = new THREE.Vector3( 0, 1, 0 );
var unitZ = new THREE.Vector3( 0, 0, 1 );
var quaternionX = new THREE.Quaternion();
var quaternionY = new THREE.Quaternion();
var quaternionZ = new THREE.Quaternion();
var eye = eye2.clone();
worldRotation.copy( this.planes[ "XY" ].rotation );
tempQuaternion.setFromEuler( worldRotation );
tempMatrix.makeRotationFromQuaternion( tempQuaternion ).getInverse( tempMatrix );
eye.applyMatrix4( tempMatrix );
this.traverse( function( child ) {
tempQuaternion.setFromEuler( worldRotation );
if ( child.name === "X" ) {
quaternionX.setFromAxisAngle( unitX, Math.atan2( - eye.y, eye.z ) );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
child.quaternion.copy( tempQuaternion );
}
if ( child.name === "Y" ) {
quaternionY.setFromAxisAngle( unitY, Math.atan2( eye.x, eye.z ) );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionY );
child.quaternion.copy( tempQuaternion );
}
if ( child.name === "Z" ) {
quaternionZ.setFromAxisAngle( unitZ, Math.atan2( eye.y, eye.x ) );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionZ );
child.quaternion.copy( tempQuaternion );
}
} );
};
this.init();
};
THREE.TransformGizmoRotate.prototype = Object.create( THREE.TransformGizmo.prototype );
THREE.TransformGizmoRotate.prototype.constructor = THREE.TransformGizmoRotate;
THREE.TransformGizmoScale = function () {
THREE.TransformGizmo.call( this );
var arrowGeometry = new THREE.Geometry();
var mesh = new THREE.Mesh( new THREE.BoxGeometry( 0.125, 0.125, 0.125 ) );
mesh.position.y = 0.5;
mesh.updateMatrix();
arrowGeometry.merge( mesh.geometry, mesh.matrix );
var lineXGeometry = new THREE.BufferGeometry();
lineXGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) );
var lineYGeometry = new THREE.BufferGeometry();
lineYGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );
var lineZGeometry = new THREE.BufferGeometry();
lineZGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );
this.handleGizmos = {
X: [
[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0xff0000 } ) ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ],
[ new THREE.Line( lineXGeometry, new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
],
Y: [
[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x00ff00 } ) ), [ 0, 0.5, 0 ] ],
[ new THREE.Line( lineYGeometry, new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
],
Z: [
[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x0000ff } ) ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ] ],
[ new THREE.Line( lineZGeometry, new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
],
XYZ: [
[ new THREE.Mesh( new THREE.BoxGeometry( 0.125, 0.125, 0.125 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ) ]
]
};
this.pickerGizmos = {
X: [
[ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
],
Y: [
[ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0.6, 0 ] ]
],
Z: [
[ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ] ]
],
XYZ: [
[ new THREE.Mesh( new THREE.BoxGeometry( 0.4, 0.4, 0.4 ), pickerMaterial ) ]
]
};
this.setActivePlane = function ( axis, eye ) {
var tempMatrix = new THREE.Matrix4();
eye.applyMatrix4( tempMatrix.getInverse( tempMatrix.extractRotation( this.planes[ "XY" ].matrixWorld ) ) );
if ( axis === "X" ) {
this.activePlane = this.planes[ "XY" ];
if ( Math.abs( eye.y ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "XZ" ];
}
if ( axis === "Y" ) {
this.activePlane = this.planes[ "XY" ];
if ( Math.abs( eye.x ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "YZ" ];
}
if ( axis === "Z" ) {
this.activePlane = this.planes[ "XZ" ];
if ( Math.abs( eye.x ) > Math.abs( eye.y ) ) this.activePlane = this.planes[ "YZ" ];
}
if ( axis === "XYZ" ) this.activePlane = this.planes[ "XYZE" ];
};
this.init();
};
THREE.TransformGizmoScale.prototype = Object.create( THREE.TransformGizmo.prototype );
THREE.TransformGizmoScale.prototype.constructor = THREE.TransformGizmoScale;
THREE.TransformControls = function ( camera, domElement ) {
// TODO: Make non-uniform scale and rotate play nice in hierarchies
// TODO: ADD RXYZ contol
THREE.Object3D.call( this );
domElement = ( domElement !== undefined ) ? domElement : document;
this.object = undefined;
this.visible = false;
this.snap = null;
this.space = "world";
this.size = 1;
this.axis = null;
var scope = this;
var _mode = "translate";
var _dragging = false;
var _plane = "XY";
var _gizmo = {
"translate": new THREE.TransformGizmoTranslate(),
"rotate": new THREE.TransformGizmoRotate(),
"scale": new THREE.TransformGizmoScale()
};
for ( var type in _gizmo ) {
var gizmoObj = _gizmo[ type ];
gizmoObj.visible = ( type === _mode );
this.add( gizmoObj );
}
var changeEvent = { type: "change" };
var mouseDownEvent = { type: "mouseDown" };
var mouseUpEvent = { type: "mouseUp", mode: _mode };
var objectChangeEvent = { type: "objectChange" };
var ray = new THREE.Raycaster();
var pointerVector = new THREE.Vector2();
var point = new THREE.Vector3();
var offset = new THREE.Vector3();
var rotation = new THREE.Vector3();
var offsetRotation = new THREE.Vector3();
var scale = 1;
var lookAtMatrix = new THREE.Matrix4();
var eye = new THREE.Vector3();
var tempMatrix = new THREE.Matrix4();
var tempVector = new THREE.Vector3();
var tempQuaternion = new THREE.Quaternion();
var unitX = new THREE.Vector3( 1, 0, 0 );
var unitY = new THREE.Vector3( 0, 1, 0 );
var unitZ = new THREE.Vector3( 0, 0, 1 );
var quaternionXYZ = new THREE.Quaternion();
var quaternionX = new THREE.Quaternion();
var quaternionY = new THREE.Quaternion();
var quaternionZ = new THREE.Quaternion();
var quaternionE = new THREE.Quaternion();
var oldPosition = new THREE.Vector3();
var oldScale = new THREE.Vector3();
var oldRotationMatrix = new THREE.Matrix4();
var parentRotationMatrix = new THREE.Matrix4();
var parentScale = new THREE.Vector3();
var worldPosition = new THREE.Vector3();
var worldRotation = new THREE.Euler();
var worldRotationMatrix = new THREE.Matrix4();
var camPosition = new THREE.Vector3();
var camRotation = new THREE.Euler();
domElement.addEventListener( "mousedown", onPointerDown, false );
domElement.addEventListener( "touchstart", onPointerDown, false );
domElement.addEventListener( "mousemove", onPointerHover, false );
domElement.addEventListener( "touchmove", onPointerHover, false );
domElement.addEventListener( "mousemove", onPointerMove, false );
domElement.addEventListener( "touchmove", onPointerMove, false );
domElement.addEventListener( "mouseup", onPointerUp, false );
domElement.addEventListener( "mouseout", onPointerUp, false );
domElement.addEventListener( "touchend", onPointerUp, false );
domElement.addEventListener( "touchcancel", onPointerUp, false );
domElement.addEventListener( "touchleave", onPointerUp, false );
this.dispose = function () {
domElement.removeEventListener( "mousedown", onPointerDown );
domElement.removeEventListener( "touchstart", onPointerDown );
domElement.removeEventListener( "mousemove", onPointerHover );
domElement.removeEventListener( "touchmove", onPointerHover );
domElement.removeEventListener( "mousemove", onPointerMove );
domElement.removeEventListener( "touchmove", onPointerMove );
domElement.removeEventListener( "mouseup", onPointerUp );
domElement.removeEventListener( "mouseout", onPointerUp );
domElement.removeEventListener( "touchend", onPointerUp );
domElement.removeEventListener( "touchcancel", onPointerUp );
domElement.removeEventListener( "touchleave", onPointerUp );
};
this.attach = function ( object ) {
this.object = object;
this.visible = true;
this.update();
};
this.detach = function () {
this.object = undefined;
this.visible = false;
this.axis = null;
};
this.setMode = function ( mode ) {
_mode = mode ? mode : _mode;
if ( _mode === "scale" ) scope.space = "local";
for ( var type in _gizmo ) _gizmo[ type ].visible = ( type === _mode );
this.update();
scope.dispatchEvent( changeEvent );
};
this.setSnap = function ( snap ) {
scope.snap = snap;
};
this.setSize = function ( size ) {
scope.size = size;
this.update();
scope.dispatchEvent( changeEvent );
};
this.setSpace = function ( space ) {
scope.space = space;
this.update();
scope.dispatchEvent( changeEvent );
};
this.update = function () {
if ( scope.object === undefined ) return;
scope.object.updateMatrixWorld();
worldPosition.setFromMatrixPosition( scope.object.matrixWorld );
worldRotation.setFromRotationMatrix( tempMatrix.extractRotation( scope.object.matrixWorld ) );
camera.updateMatrixWorld();
camPosition.setFromMatrixPosition( camera.matrixWorld );
camRotation.setFromRotationMatrix( tempMatrix.extractRotation( camera.matrixWorld ) );
scale = worldPosition.distanceTo( camPosition ) / 6 * scope.size;
this.position.copy( worldPosition );
// NOTE: we are commenting this line because this transformation looks
// confusing when using an ortographic camera
// this.scale.set( scale, scale, scale );
eye.copy( camPosition ).sub( worldPosition ).normalize();
if ( scope.space === "local" ) {
_gizmo[ _mode ].update( worldRotation, eye );
} else if ( scope.space === "world" ) {
_gizmo[ _mode ].update( new THREE.Euler(), eye );
}
_gizmo[ _mode ].highlight( scope.axis );
};
function onPointerHover( event ) {
if ( scope.object === undefined || _dragging === true || ( event.button !== undefined && event.button !== 0 ) ) return;
var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;
var intersect = intersectObjects( pointer, _gizmo[ _mode ].pickers.children );
var axis = null;
if ( intersect ) {
axis = intersect.object.name;
event.preventDefault();
}
if ( scope.axis !== axis ) {
scope.axis = axis;
scope.update();
scope.dispatchEvent( changeEvent );
}
}
function onPointerDown( event ) {
if ( scope.object === undefined || _dragging === true || ( event.button !== undefined && event.button !== 0 ) ) return;
var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;
if ( pointer.button === 0 || pointer.button === undefined ) {
var intersect = intersectObjects( pointer, _gizmo[ _mode ].pickers.children );
if ( intersect ) {
event.preventDefault();
event.stopPropagation();
scope.dispatchEvent( mouseDownEvent );
scope.axis = intersect.object.name;
scope.update();
eye.copy( camPosition ).sub( worldPosition ).normalize();
_gizmo[ _mode ].setActivePlane( scope.axis, eye );
var planeIntersect = intersectObjects( pointer, [ _gizmo[ _mode ].activePlane ] );
if ( planeIntersect ) {
oldPosition.copy( scope.object.position );
oldScale.copy( scope.object.scale );
oldRotationMatrix.extractRotation( scope.object.matrix );
worldRotationMatrix.extractRotation( scope.object.matrixWorld );
parentRotationMatrix.extractRotation( scope.object.parent.matrixWorld );
parentScale.setFromMatrixScale( tempMatrix.getInverse( scope.object.parent.matrixWorld ) );
offset.copy( planeIntersect.point );
}
}
}
_dragging = true;
}
function onPointerMove( event ) {
if ( scope.object === undefined || scope.axis === null || _dragging === false || ( event.button !== undefined && event.button !== 0 ) ) return;
var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;
var planeIntersect = intersectObjects( pointer, [ _gizmo[ _mode ].activePlane ] );
if ( planeIntersect === false ) return;
event.preventDefault();
event.stopPropagation();
point.copy( planeIntersect.point );
if ( _mode === "translate" ) {
point.sub( offset );
point.multiply( parentScale );
if ( scope.space === "local" ) {
point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
if ( scope.axis.search( "X" ) === - 1 ) point.x = 0;
if ( scope.axis.search( "Y" ) === - 1 ) point.y = 0;
if ( scope.axis.search( "Z" ) === - 1 ) point.z = 0;
point.applyMatrix4( oldRotationMatrix );
scope.object.position.copy( oldPosition );
scope.object.position.add( point );
}
if ( scope.space === "world" || scope.axis.search( "XYZ" ) !== - 1 ) {
if ( scope.axis.search( "X" ) === - 1 ) point.x = 0;
if ( scope.axis.search( "Y" ) === - 1 ) point.y = 0;
if ( scope.axis.search( "Z" ) === - 1 ) point.z = 0;
point.applyMatrix4( tempMatrix.getInverse( parentRotationMatrix ) );
scope.object.position.copy( oldPosition );
scope.object.position.add( point );
}
if ( scope.snap !== null ) {
if ( scope.axis.search( "X" ) !== - 1 ) scope.object.position.x = Math.round( scope.object.position.x / scope.snap ) * scope.snap;
if ( scope.axis.search( "Y" ) !== - 1 ) scope.object.position.y = Math.round( scope.object.position.y / scope.snap ) * scope.snap;
if ( scope.axis.search( "Z" ) !== - 1 ) scope.object.position.z = Math.round( scope.object.position.z / scope.snap ) * scope.snap;
}
} else if ( _mode === "scale" ) {
point.sub( offset );
point.multiply( parentScale );
if ( scope.space === "local" ) {
if ( scope.axis === "XYZ" ) {
scale = 1 + ( ( point.y ) / 50 );
scope.object.scale.x = oldScale.x * scale;
scope.object.scale.y = oldScale.y * scale;
scope.object.scale.z = oldScale.z * scale;
} else {
point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
if ( scope.axis === "X" ) scope.object.scale.x = oldScale.x * ( 1 + point.x / 50 );
if ( scope.axis === "Y" ) scope.object.scale.y = oldScale.y * ( 1 + point.y / 50 );
if ( scope.axis === "Z" ) scope.object.scale.z = oldScale.z * ( 1 + point.z / 50 );
}
}
} else if ( _mode === "rotate" ) {
point.sub( worldPosition );
point.multiply( parentScale );
tempVector.copy( offset ).sub( worldPosition );
tempVector.multiply( parentScale );
if ( scope.axis === "E" ) {
point.applyMatrix4( tempMatrix.getInverse( lookAtMatrix ) );
tempVector.applyMatrix4( tempMatrix.getInverse( lookAtMatrix ) );
rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );
tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );
quaternionE.setFromAxisAngle( eye, rotation.z - offsetRotation.z );
quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionE );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );
scope.object.quaternion.copy( tempQuaternion );
} else if ( scope.axis === "XYZE" ) {
quaternionE.setFromEuler( point.clone().cross( tempVector ).normalize() ); // rotation axis
tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );
quaternionX.setFromAxisAngle( quaternionE, - point.clone().angleTo( tempVector ) );
quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );
scope.object.quaternion.copy( tempQuaternion );
} else if ( scope.space === "local" ) {
point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
tempVector.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );
rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );
quaternionXYZ.setFromRotationMatrix( oldRotationMatrix );
quaternionX.setFromAxisAngle( unitX, rotation.x - offsetRotation.x );
quaternionY.setFromAxisAngle( unitY, rotation.y - offsetRotation.y );
quaternionZ.setFromAxisAngle( unitZ, rotation.z - offsetRotation.z );
if ( scope.axis === "X" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionX );
if ( scope.axis === "Y" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionY );
if ( scope.axis === "Z" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionZ );
scope.object.quaternion.copy( quaternionXYZ );
} else if ( scope.space === "world" ) {
rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );
tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );
quaternionX.setFromAxisAngle( unitX, rotation.x - offsetRotation.x );
quaternionY.setFromAxisAngle( unitY, rotation.y - offsetRotation.y );
quaternionZ.setFromAxisAngle( unitZ, rotation.z - offsetRotation.z );
quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );
if ( scope.axis === "X" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
if ( scope.axis === "Y" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionY );
if ( scope.axis === "Z" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionZ );
tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );
scope.object.quaternion.copy( tempQuaternion );
}
}
scope.update();
scope.dispatchEvent( changeEvent );
scope.dispatchEvent( objectChangeEvent );
}
function onPointerUp( event ) {
if ( event.button !== undefined && event.button !== 0 ) return;
if ( _dragging && ( scope.axis !== null ) ) {
mouseUpEvent.mode = _mode;
scope.dispatchEvent( mouseUpEvent )
}
_dragging = false;
onPointerHover( event );
}
function intersectObjects( pointer, objects ) {
var rect = domElement.getBoundingClientRect();
var x = ( pointer.clientX - rect.left ) / rect.width;
var y = ( pointer.clientY - rect.top ) / rect.height;
pointerVector.set( ( x * 2 ) - 1, - ( y * 2 ) + 1 );
ray.setFromCamera( pointerVector, camera );
var intersections = ray.intersectObjects( objects, true );
return intersections[ 0 ] ? intersections[ 0 ] : false;
}
};
THREE.TransformControls.prototype = Object.create( THREE.Object3D.prototype );
THREE.TransformControls.prototype.constructor = THREE.TransformControls;
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 );
}() );
初始化三维场景,并增加点云、三个模型和控制功能
javascript
//
// Utils 工具
//
var Utils = {};
Utils.createShadow = function (mesh, material) {
var params = mesh.geometry.parameters;
mesh.geometry.computeBoundingSphere();
var geo = mesh.geometry.type === 'BoxGeometry'
? new THREE.PlaneGeometry(params.width, params.depth)
: new THREE.CircleGeometry(mesh.geometry.boundingSphere.radius, 24);
var shadow = new THREE.Mesh(geo, material);
shadow.rotation.x = -Math.PI / 2;
shadow.position.x = mesh.position.x;
shadow.position.z = mesh.position.z;
return shadow;
};
Utils.updateShadow = function (shadow, target) {
shadow.position.x = target.position.x;
shadow.position.z = target.position.z;
shadow.visible = target.position.y >= 0;
shadow.scale.x = target.scale.x;
shadow.scale.y = target.scale.z;
};
function init(){
var WIDTH = 800;
var HEIGHT = 600;
this._previousElapsed = 0;
// setup a WebGL renderer within an existing canvas
var canvas = document.getElementById('demo');
// Renderer渲染器设置
this.renderer = new THREE.WebGLRenderer({
//抗锯齿属性,WebGLRenderer常用的一个属性
antialias:true,
alpha:true,
canvas: canvas
});
this.renderer.setClearColor(0xb9d3ff, 0.2); //设置背景颜色和透明度
canvas.width = WIDTH;
canvas.height = HEIGHT;
this.renderer.setViewport(0, 0, WIDTH, HEIGHT);
// create the scene Scene设置
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color('#e6e6e6');
const fov = 40 // 视野范围
const aspect = 2 // 相机默认值 画布的宽高比
const near = 0.1 // 近平面
const far = 1000 // 远平面
// 透视投影相机
this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
// create an isometric camera 正交相机
//this.camera = new THREE.OrthographicCamera(-5, 5, 5, -5, -1, 100);
this.camera.position.set(10, 10, 10)
this.camera.lookAt(this.scene.position); // point at origin
//this.camera.lookAt(0, 0, 0)
// create ground and axis / grid helpers
var ground = new THREE.Mesh(new THREE.PlaneGeometry(10, 10),new THREE.MeshBasicMaterial({color: 0xcccccc}));
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.01; // to avoid z-fighting with axis and shadows
this.scene.add(ground);
this.scene.add((new THREE.AxesHelper(8)));
// 光源
const lightColor = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(lightColor, intensity);
this.scene.add(light);
// 材质
this.materials = {
shadow: new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
opacity: 0.5
}),
solid: new THREE.MeshNormalMaterial({}),
colliding: new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.5
}),
dot: new THREE.MeshBasicMaterial({
color: 0x0000ff
})
};
this.knot = new THREE.Mesh(new THREE.TorusKnotGeometry(0.5, 0.1), this.materials.solid);
this.knot.position.set(-3, 2, 1);
this.knot.geometry.computeBoundingSphere();
this.sphere = new THREE.Mesh(new THREE.SphereGeometry(1), this.materials.solid);
this.sphere.position.set(2, 2, 0);
this.sphereShadow = Utils.createShadow(this.sphere, this.materials.shadow);
// the object the user can control to check for collisions
this.cube = new THREE.Mesh(new THREE.BoxGeometry(0.75, 0.75, 0.75),this.materials.solid);
this.cube.position.set(2, 2, 1.74);
this.cubeShadow = Utils.createShadow(this.cube, this.materials.shadow);
// add objects to the scene
this.scene.add(this.cube);
this.scene.add(this.knot);
this.scene.add(this.sphere);
// add fake shadows to the scene
this.scene.add(Utils.createShadow(this.knot, this.materials.shadow));
this.scene.add(this.sphereShadow);
this.scene.add(this.cubeShadow);
// 添加相机控制器
this.controls = new THREE.OrbitControls( this.camera, this.renderer.domElement );
// 开启控制器的阻尼效果
this.controls.enableDamping = true
this.scene.add(this.controls);
this.transFormControls = new THREE.TransformControls(this.camera, this.renderer.domElement);
this.transFormControls.space = 'world';
this.transFormControls.attach(this.cube);
this.scene.add(this.transFormControls);
this.timestamp = 0;
function render(elapsed){
window.requestAnimationFrame(render);
// compute delta time in seconds -- also cap it
var delta = (elapsed - this._previousElapsed) / 1000.0;
delta = Math.min(delta, 0.25); // maximum delta of 250 ms
this._previousElapsed = elapsed;
this.update(delta);
this.renderer.render(this.scene, this.camera);
}
this.world = new CANNON.World();
this.addPhysicalBody = function (mesh, bodyOptions) {
var shape;
// create a Sphere shape for spheres and thorus knots,
// a Box shape otherwise
if (mesh.geometry.type === 'SphereGeometry' || mesh.geometry.type === 'ThorusKnotGeometry') {
mesh.geometry.computeBoundingSphere();
shape = new CANNON.Sphere(mesh.geometry.boundingSphere.radius);
}
else {
mesh.geometry.computeBoundingBox();
var box = mesh.geometry.boundingBox;
shape = new CANNON.Box(new CANNON.Vec3(
(box.max.x - box.min.x) / 2,
(box.max.y - box.min.y) / 2,
(box.max.z - box.min.z) / 2
));
}
var body = new CANNON.Body(bodyOptions);
body.addShape(shape);
body.position.copy(mesh.position);
body.computeAABB();
// disable collision response so objects don't move when they collide
// against each other
body.collisionResponse = false;
// keep a reference to the mesh so we can update its properties later
body.mesh = mesh;
this.world.addBody(body);
return body;
};
this.initPhysicalWorld = function() {
// add physical bodies
this.knotBody = this.addPhysicalBody(this.knot, {mass: 1});
this.addPhysicalBody(this.sphere, {mass: 1});
this.cubeBody = this.addPhysicalBody(this.cube, {mass: 1});
// register for collide events
this.cubeBody.addEventListener('collide', function (e) {
console.log('Collision!');
}.bind(this));
// rotate the knot
this.knotBody.angularVelocity.x = Math.PI / 4;
};
this.update = function (delta) {
this.timestamp += delta;
// move the cube body with mouse controls
this.controls.update();
this.transFormControls.update();
this.cubeBody.position.copy(this.cube.position);
// update the cube shadow
Utils.updateShadow(this.cubeShadow, this.cube);
// update knot mesh rotation
this.knot.quaternion.copy(this.knotBody.quaternion);
// reset materials
this.sphere.material = this.materials.solid;
this.knot.material = this.materials.solid;
this.updatePhysics(delta);
};
this.updatePhysics = function (delta) {
this.world.step(delta);
this.world.contacts.forEach(function (contact) {
contact.bi.mesh.material = this.materials.colliding;
contact.bj.mesh.material = this.materials.colliding;
this.cube.material = this.materials.solid;
}.bind(this));
};
// setup physic world
this.initPhysicalWorld();
this.addPointCloud = function(){
// 彩色点云
var particles = 50 * 1000;
var geometry = new THREE.BufferGeometry();
// 生成 5万个点需要的存储空间
var positions = new Float32Array(particles * 3);
// 每个顶点一种颜色
var colors = new Float32Array(particles * 3);
var color = new THREE.Color();
var n = 4, n2 = n / 2; // 限定点出现的范围是[-2,2]这么一个立方体中,n2表示直径的一半
for (var i = 0; i < positions.length; i += 9) {
// 通过随机数生成点的位置
// 生成一个顶点,范围是[-2,2]
var x = Math.random() * n - n2;
var y = Math.random() * n - n2;
var z = Math.random() * n - n2;
// 随机生成点
positions[i] = x;
positions[i + 1] = y;
positions[i + 2] = z;
// 为每个顶点赋值颜色
// x / n得到范围[-0.5,0.5],加0.5得到[0,1]范围的颜色
var vx = (x / n) + 0.5
var vy = (y / n) + 0.5
var vz = (z / n) + 0.5
color.setRGB(vx, vy, vz)
colors[i] = color.r
colors[i + 1] = color.g
colors[i + 2] = color.b
}
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3))
// 计算几何体的包围盒
geometry.computeBoundingSphere();
// 创建点材质
var material = new THREE.PointsMaterial({
size: 0.1,
vertexColors: THREE.VertexColors
})
// 创建点
particleSystem = new THREE.Points(geometry, material);
this.scene.add(particleSystem);
}
this.addPointCloud();
window.requestAnimationFrame(render);
}
//
// main
//
window.onload = function () {
try {
init();
} catch (error) {
console.error(error);
// Expected output: ReferenceError: nonExistentFunction is not defined
// (Note: the exact output may be browser-dependent)
}
};
实现Web 3D效果
移动盒子,通过物理引擎cannon.js实现碰撞检测。
javascript
this.world = new CANNON.World();
this.addPhysicalBody = function (mesh, bodyOptions) {
var shape;
// create a Sphere shape for spheres and thorus knots,
// a Box shape otherwise
if (mesh.geometry.type === 'SphereGeometry' || mesh.geometry.type === 'ThorusKnotGeometry') {
mesh.geometry.computeBoundingSphere();
shape = new CANNON.Sphere(mesh.geometry.boundingSphere.radius);
}
else {
mesh.geometry.computeBoundingBox();
var box = mesh.geometry.boundingBox;
shape = new CANNON.Box(new CANNON.Vec3(
(box.max.x - box.min.x) / 2,
(box.max.y - box.min.y) / 2,
(box.max.z - box.min.z) / 2
));
}
var body = new CANNON.Body(bodyOptions);
body.addShape(shape);
body.position.copy(mesh.position);
body.computeAABB();
// disable collision response so objects don't move when they collide
// against each other
body.collisionResponse = false;
// keep a reference to the mesh so we can update its properties later
body.mesh = mesh;
this.world.addBody(body);
return body;
};
this.initPhysicalWorld = function() {
// add physical bodies
this.knotBody = this.addPhysicalBody(this.knot, {mass: 1});
this.addPhysicalBody(this.sphere, {mass: 1});
this.cubeBody = this.addPhysicalBody(this.cube, {mass: 1});
// register for collide events
this.cubeBody.addEventListener('collide', function (e) {
console.log('Collision!');
}.bind(this));
// rotate the knot
this.knotBody.angularVelocity.x = Math.PI / 4;
};
HTML结构
html
<!DOCTYPE html>
<html>
<head>
<title>three.js+cannon.js Web 3D</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<meta name="renderer" content="webkit">
<meta name="force-rendering" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=10,chrome=1">
<meta data-rh="true" name="keywords" content="three.js,JavaScript,canon.js">
<meta data-rh="true" name="description" content="three.js+cannon.js Web 3D">
<meta data-rh="true" property="og:title" content="THREE.JS and CANON.JS">
<meta data-rh="true" property="og:url" content="">
<meta data-rh="true" property="og:description" content="three.js+cannon.js Web 3D">
<meta data-rh="true" property="og:image" content="">
<meta data-rh="true" property="og:type" content="article">
<meta data-rh="true" property="og:site_name" content="">
<link rel="stylesheet" href="styles.css" type="text/css">
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/109/three.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
<script src="js/lib/TransformControls.js"></script>
<script src="js/index.js"></script>
</head>
<body>
<header class="main-header">
<h1><a href="index.html">Web 3D</a></h1>
<p>Using cannon.js (physics engine)</p>
</header>
<!-- main wrapper -->
<div class="main-wrapper">
<!-- canvas wrapper -->
<div class="canvas-wrapper">
<canvas id="demo"></canvas>
<aside class="message">
<strong>Drag</strong> the cube around
</aside>
</div>
<!-- canvas wrapper -->
<footer class="main-footer">
<p>Source code</p>
</footer>
</div>
<!-- main wrapper -->
</body>
</html>
CSS样式
css
body {
font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
margin: 0;
}
canvas {
background: transparent;
padding: 0;
margin: 0;
border: 5px solid #31ffd5;
}
a, a:hover, a:visited {
color: #000;
text-decoration: none;
}
a {
border-bottom: 2px solid #31ffd5;
}
a:hover {
background:#31ffd5;
}
.main-wrapper {
margin: 0 auto;
text-align: center;
}
.main-header, .main-footer {
text-align: center;
}
.subtitle {
font-size: 1.5em;
}
.main-header {
margin-top: 0;
margin-bottom: 2em;
transform: perspective(750px) translate3d(0px, 0px, -250px) rotateX(27deg) scale(0.96, 0.96);
border-radius: 20px;
border: 5px solid #e6e6e6;
box-shadow: 0 70px 40px -20px rgba(0, 0, 0, 0.2);
transition: 0.4s ease-in-out transform;
}
.main-header h1 {
margin: 0;
background: #31ffd5;
padding: 0.5em 1em;
}
.main-header a {
color: #000;
text-decoration: none;
}
.main-footer {
margin-top: 2em;
}
.canvas-wrapper {
position: relative;
background: #12345611;
}
.message {
position: absolute;
color: #fff;
bottom: 0;
left: 0;
right: 0;
padding: 5px;
text-align: center;
}
.message strong {
color: #31ffd5;
}
li, p {
line-height: 1.25em;
}
li {
margin: 0.5em 0;
}
cannon.js
cannon.js是一个轻量级且简单的网络三维物理引擎。受three.js和ammon.js的启发,加上网络缺乏物理引擎,cannon.js应运而生。刚体物理引擎包括简单的碰撞检测、各种体型、接触、摩擦和约束。文件大小比许多移植的物理引擎更小。100%开源JavaScript,使用迭代高斯-塞德尔Gauss-Seidel解算器来求解约束。使用SPOOK步进器。
https://schteppe.github.io/cannon.js/
Three.js 官方文档
Three.js 官方文档是学习和使用 Three.js 这一基于 WebGL 的 JavaScript 3D 图形库的重要资源。它提供了丰富的信息,包括各种功能
、类
、方法
和属性
的详细描述,以及许多示例和教程。
https://threejs.org/docs/index.html
Three.js 中文网
Three.js 中文网是一个专注于 Three.js 资源分享与交流的中文社区网站。该网站提供了大量与 Three.js 相关的教程
、案例
、插件
和工具
,帮助开发者更好地学习和应用 Three.js。
Three.js简版案例
Three.js简版案例集合的目标是提供一组基本的、有指导意义的示例,介绍Three.js中的各种功能。每个页面的源代码都包含详细的注释。
https://stemkoski.github.io/Three.js/
探索three.js
探索three.js
是对 web 作为 3D 图形平台的完整介绍,它使用 three.js WebGL 库,编写自一位核心 three.js 开发人员。可以作为一个引导读者深入了解 three.js 库的指南。
https://discoverthreejs.com/zh/
three.js editor
three.js editor
是一个基于 Three.js 库的在线3D编辑器
。这个编辑器允许用户直接在网页上创建和编辑 3D 场景。在 Three.js 编辑器中,你可以:
-
创建和编辑 3D 对象:通过编辑器提供的工具,你可以创建各种几何体(如立方体、球体、圆柱体等),并调整它们的位置、旋转和缩放。
-
添加材质和贴图:为 3D 对象添加不同的材质,如基本材质、物理材质等,并可以应用贴图来改变对象的外观。
-
设置光源和环境:在场景中添加不同类型的光源(如点光源、平行光源、环境光等),并调整光照参数。
-
实时预览:编辑器会实时渲染你的 3D 场景,让你可以立即看到所做的更改。
-
导入和导出模型:支持导入和导出多种 3D 文件格式,如 glTF、OBJ 等。
Shadertoy
Shadertoy是一个基于 WebGL 的在线实时渲染平台,主要用于编辑
、分享
和查看
着色器shader 程序及其实现的效果。
在这个平台上,用户可以创作和分享自己的 3D 图形效果。它提供了一个简单方便的环境,让用户可以轻松编辑自己的片段着色器,并实时查看修改的效果。
同时,Shadertoy 上有许多大佬分享他们制作的酷炫效果的代码,这些代码是完全开源的,用户可以在这些代码的基础上进行修改和学习。
除此之外,Shadertoy 还允许用户选择声音频道,将当前帧的声音信息转变成纹理(Texture),传入 shader 当中,从而根据声音信息来控制图形。这使得 Shadertoy 在视觉和听觉的结合上有了更多的可能性。
glsl.app
glsl.app
是一个在线的 GLSL (OpenGL Shading Language) 编辑器。GLSL 是一种用于图形渲染的着色语言,特别是在 OpenGL 图形库中。这种语言允许开发者为图形硬件编写着色器程序
,这些程序可以运行在 GPU 上,用于计算图像的各种视觉效果。在 glsl.app 上,你可以:
-
编写和编辑着色器代码:直接在网页上编写顶点着色器、片元着色器等。
-
实时预览:当你编写或修改着色器代码时,可以立即在右侧的预览窗口中看到效果。
-
分享你的作品:完成你的着色器后,你可以获得一个链接,通过这个链接与其他人分享你的作品。
-
学习:如果你是初学者,该网站还提供了很多示例和教程,帮助你了解如何编写各种着色器效果。
Online WebGL (GLSL) Shaders Editor and Playground
参见:
Mozilla Developer Relations · GitHub
Beautiful CSS 3D Transform Perspective Examples in 2024 | Polypane