画虚线:
function DrawDashLine(){
var geometry = new THREE.Geometry();
/**
* vertexColors: false 关闭使用点颜色来生成线的颜色,这个值默认是false,如果设置为true,那么后面设置的color将不起作用
* dashSize:30 点长度30个单位
* gapSize:20 点与点之间间隔长度20个单位
* @type {THREE.LineDashedMaterial}
*/
var material = new THREE.LineDashedMaterial({ vertexColors: false,dashSize:30,gapSize:20,color:0x839848});
var p1 = new THREE.Vector3(0, 0,0);
var p2 = new THREE.Vector3(500,300,0);
geometry.vertices.push(p1);
geometry.vertices.push(p2);
/**
* 记得加上这句,用于计算上面设置的点和间隔长度,不设置则画出来是实线
*/
geometry.computeLineDistances();
var line = new THREE.Line(geometry,material,THREE.LineSegments );
scene.add(line);
}
注意:threejs定义了很多computeXXX方法和XXXneedsUpdate属性,这表示XXX是默认不更新的,你要想获得其真实值,就必须手动调用computeXXX方法或设置XXXneedsUpdate属性。threejs这么做,是出于性能考虑
给立方体画上虚线边框:
思路,遍历立方体的所有顶点,对每一个顶点,画上与其相接的三条边,每条边画一半长度。
function drawUnVisiableBorder(){
for(var i=0 ; i< cube.geometry.vertices.length ; i ++ ){
var v1 = new THREE.Vector3();
v1.copy(cube.geometry.vertices[i]);
var v2 = new THREE.Vector3();
v2.copy(v1);
v2.x = -v2.x/2;
DrawDashLine(v1,v2);
var v2 = new THREE.Vector3();
v2.copy(v1);
v2.y = -v2.y/2;
DrawDashLine(v1,v2);
var v2 = new THREE.Vector3();
v2.copy(v1);
v2.z = -v2.z/2;
DrawDashLine(v1,v2);
}
}
注意:画虚线只能画一次,如果画两次,第一次从A到B,第二次从B到A,虚线就会变成实线,因为两次画的线段和空缺刚好互补
另外,新的threejs提供了EdgesGeometry,也可以用来描边
在Threejs中,线本身是二维的,没有办法设置粗细,这样要选择、操作一条线就不太好操作。这时,可以在线外面包一层不可见的圆柱,这样线就很好操作了。
function drawCylinder(topRadius,bottomRadius ,height){
//四个参数分别是上底面半径、下底面半径,高度,半径段数
var geometry = new THREE.CylinderGeometry(topRadius,bottomRadius,height,16);
var material = new THREE.MeshBasicMaterial({visible:true,color:0x879378});
var cylinder = new THREE.Mesh(geometry,material);
return cylinder;
}
纯色的线:
function colorLine(){
var geometry = new THREE.Geometry();
/**
* vertexColors: false,表示使用color来设置颜色
* @type {THREE.LineBasicMaterial}
*/
var material = new THREE.LineBasicMaterial({ vertexColors: false,color:#cccccc });
var p1 = new THREE.Vector3(0, 0,0);
var p2 = new THREE.Vector3(500,300,0);
geometry.vertices.push(p1);
geometry.vertices.push(p2);
var line = new THREE.Line(geometry,material,THREE.LineSegments );
scene.add(line);
}
渐变线:使用端点的颜色,从端点A的颜色变化到端点B的颜色
function initObject(){
var geometry = new THREE.Geometry();
/**
* vertexColors: true 表示使用端点颜色
* @type {THREE.LineBasicMaterial}
*/
var material = new THREE.LineBasicMaterial({ vertexColors: true });
var p1 = new THREE.Vector3(0, 0,0);
var p2 = new THREE.Vector3(500,300,0);
geometry.vertices.push(p1);
geometry.vertices.push(p2);
/**
* 将端点颜色加到geometry.colors中,这是一个数组,和端点数组一一对应
*/
var color1 = new THREE.Color(0x3966FF);
var color2 = new THREE.Color(0x629729 );
geometry.colors.push(color1,color2);
var line = new THREE.Line(geometry,material,THREE.LineSegments );
scene.add(line);
}
使用Group对象(实质就是一个Object3D对象):
var group = new THREE.Group();
function initObject(){
var obj1 = ...;
var obj2 = ...;
group.add(obj1);
group.add(obj2);
scene.add(group);
}
打包后对象可以一起操作。打包过后,对象就有里层对象和外层对象,如上面例子中group
是外层对象,obj1
和obj2
是里层对象。里层对象的参考系是外层对象,即里层对象的位置是相对于外层对象而言的。
可以使用traverse
,相比于直接使用Object3D.children
,traverse
会递归遍历所有子孙
group.traverse(function (e) {
if (e instanceof THREE.Mesh && e != plane) {
e.rotation.x += 0.01;
e.rotation.y += 0.01;
e.rotation.z += 0.01;
}
});
在threejs里,物体都有两个面,里面和外面,并且两个面可以独立设置可见性。
var meshMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff});
//下面三个语句只需要一个即可。
meshMaterial.side = THREE.FrontSide; //只显示外面
meshMaterial.side = THREE.BackSide; //只显示里面
meshMaterial.side = THREE.DoubleSide;//两面都显示
为了提高性能,threejs默认物体的形状和材质一经创建,就永不改变。所以,修改物体的形状和材质,就需要手动告诉threejs,方法就是使用各种computeXXX方法和needsUpdate属性。
对于形状:
各种computeXXX方法。
对于材质:
needsUpdate属性
只有需要体现在webGL上的改变才需要将该属性设置为true,一些简单属性如color,就不需要设置该属性。
function render(){
renderer.render(scene,camera);
cube.material.opacity -= 0.3;
//表示透明度小于0.4时,不显示
cube.material.alphaTest = 0.4;
//有这句时,立方体不显示,没有时,显示
cube.material.needsUpdate = true;
requestAnimationFrame(render,1000);
}
设置透明度需要设置材质的transparent:true
var material = new THREE.MeshBasicMaterial({transparent:true});
material.opacity = 0.5;
可以参考下面的实现
function onDocumentMouseWheel(event){
if(event.wheelDelta>0){ //判断滚轮滚动方向
cube.scale.multiplyScalar(1.02);
}else{
cube.scale.multiplyScalar(0.98);
}
render();
}
深度测试可以实现:远的看着模糊,近的看着清楚,被遮住的看不见等效果
例如一个立方体,怎么设置为看得见得边为实线,看不见的边为虚线?
可以给立方体每条边都画上实线和虚线,实线的depthTest
设为true
,虚线的depthTest
设为false
:
/**
* 画实线
* */
function DrawLine(v1,v2){
var geometry = new THREE.Geometry();
//设置depthTest属性为true,表示被挡住的边就看不见
var lineMaterial = new THREE.LineBasicMaterial({ vertexColors: false,depthTest:true,color:0x894834});
var p1 = new THREE.Vector3();
p1.copy(v1);
var p2 = new THREE.Vector3();
p2.copy(v2);
geometry.vertices.push(p1);
geometry.vertices.push(p2);
var line = new THREE.Line(geometry,lineMaterial,THREE.LineSegments );
return line;
}
/**
* 画虚线
* */
function DrawDashLine(v1,v2){
var geometry = new THREE.Geometry();
//设置depthTest属性为false,表示被挡住的边也看得见
var lineMaterial = new THREE.LineDashedMaterial({ color:0x010101 ,dashSize:5,gapSize:10,depthTest:false });
var p1 = new THREE.Vector3();
p1.copy(v1);
var p2 = new THREE.Vector3();
p2.copy(v2);
geometry.vertices.push(p1);
geometry.vertices.push(p2);
geometry.computeLineDistances();
var line = new THREE.Line(geometry,lineMaterial,THREE.LineSegments);
cube_Border_Group.add(line);
}
深度测试的例子,你会发现有几条边即可以看见实线,也可以看见虚线,两种重叠到一起,若隐若现。这种问题就是z-fighting导致的。因为实线和虚线靠得太近,它们的深度太接近,导致threejs无法正确的区分出谁前谁后,就会出现闪烁问题。这时可以给正方形设置polygonOffset
属性,这个属性的作用是:当两个图形在同一个像素上的深度相同时(也就是深度发生了冲突),将设置了polygonOffset
属性的图形的深度值加上一小段偏移,从而避免冲突。
/**
* 画立方体
* */
function drawCube(){
var geometry = new THREE.BoxGeometry( 100, 100,100);
//下面设置了polygonOffset:true,polygonOffsetFactor:0.5
var material = new THREE.MeshBasicMaterial({color:0xCCCCCC,transparent:false,polygonOffset:true,polygonOffsetFactor:0.5});
.....
}
关于z-fighting,请看:threejs- z-fighting 问题
/**
*使用示例:创建XYZ
* */
function createXYZText(){
//创建Z
var spritey = makeTextSprite( "Z", { fontsize: 100} );
spritey.position.set(0,10,200);
scene.add(spritey);
//创建X
spritey = makeTextSprite( "X", { fontsize: 100 } );
spritey.position.set(230,30,0);
scene.add(spritey);
//创建Y
spritey = makeTextSprite( "Y", { fontsize: 100} );
spritey.position.set(0,200,0);
scene.add(spritey);
}
/**
* 创建永远面向相机的2D文字
* */
function makeTextSprite( message, parameters )
{
if ( parameters === undefined ) parameters = {};
var fontface = parameters.hasOwnProperty("fontface") ? parameters["fontface"] : "Arial";
var fontsize = parameters.hasOwnProperty("fontsize") ? parameters["fontsize"] : 18;
var borderThickness = parameters.hasOwnProperty("borderThickness") ? parameters["borderThickness"] : 4;
var textColor = parameters.hasOwnProperty("textColor") ?parameters["textColor"] : { r:0, g:0, b:0, a:1.0 };
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
context.font = "Bold " + fontsize + "px " + fontface;
var metrics = context.measureText( message );
var textWidth = metrics.width;
context.lineWidth = borderThickness;
context.fillStyle = "rgba("+textColor.r+", "+textColor.g+", "+textColor.b+", 1.0)";
context.fillText( message, borderThickness, fontsize + borderThickness);
var texture = new THREE.Texture(canvas)
texture.needsUpdate = true;
var spriteMaterial = new THREE.SpriteMaterial( { map: texture, useScreenCoordinates: false } );
var sprite = new THREE.Sprite( spriteMaterial );
sprite.scale.set(0.5 * fontsize, 0.25 * fontsize, 0.75 * fontsize);
return sprite;
}
参考three-js-2d-text-sprite-labels
定义出点数组、面数组就可以了
var vertices = [
new THREE.Vector3(1, 3, 1),
new THREE.Vector3(1, 3, -1),
new THREE.Vector3(1, -1, 1),
new THREE.Vector3(1, -1, -1),
new THREE.Vector3(-1, 3, -1),
new THREE.Vector3(-1, 3, 1),
new THREE.Vector3(-1, -1, -1),
new THREE.Vector3(-1, -1, 1)
];
var faces = [
new THREE.Face3(0, 2, 1),
new THREE.Face3(2, 3, 1),
new THREE.Face3(4, 6, 5),
new THREE.Face3(6, 7, 5),
new THREE.Face3(4, 5, 1),
new THREE.Face3(5, 0, 1),
new THREE.Face3(7, 6, 2),
new THREE.Face3(6, 3, 2),
new THREE.Face3(5, 7, 0),
new THREE.Face3(7, 2, 0),
new THREE.Face3(1, 3, 4),
new THREE.Face3(3, 6, 4),
];
var geom = new THREE.Geometry();
geom.vertices = vertices;
geom.faces = faces;
geom.computeFaceNormals(); //注意要加这个
var materials = [
new THREE.MeshLambertMaterial({color: Math.random() * 0xffffff, shading: THREE.FlatShading}),
new THREE.MeshBasicMaterial({color: 0x000000, wireframe: true})
];
var mesh = THREE.SceneUtils.createMultiMaterialObject(geometry, materials);
当控制面板上的“position”文件夹下的”positionX”改变时,触发事件
var gui = new dat.GUI();
guiPosition = gui.addFolder('position');
var contX = guiPosition.add(controls, 'positionX', -10, 10);
contX.listen();
contX.onChange(function (value) {
cube.position.x = controls.positionX;
});
需要引入:
<script type="text/javascript" src="../libs/dat.gui.js">script>
创建renderer的时候加上antialias
参数
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setClearColor(0x348934,0.7);
第一个参数是颜色,第二个参数是透明度。屏幕背景默认是黑色的,所以透明度的值越小,屏幕越暗。
scene.add( new THREE.GridHelper( 500, 25 ) );
添加上面语句之后,屏幕中心会显示一个边长为500的水平面,水平面规格为25*25,threejs有很多helper,可以查阅官方文档
也就是将屏幕坐标转成threejs空间坐标。
function transform(x,y){
let mouse = new THREE.Vector2();
mouse.x = x-window.innerWidth/2;
mouse.y = window.innerHeight/2 - y;
console.log(x+"->"+mouse.x);
console.log(y+"->"+mouse.y);
}
原理请看【对象拾取】
可以使用Raycaster
对象获得鼠标点击处的对象数组。这个对象可以从某个位置(一般是相机)发射一条射线,穿过鼠标点击的位置,返回所有相交的对象。
/**首先,要给document注册事件*/
function initRenerer(){
renderer = new THREE.WebGLRenderer({antialias : true});
renderer.setSize(window.innerWidth,window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.setClearColor(0xFFFFFF,1.0);
document.addEventListener("mousedown",onMouseDown,false);
}
/**使用射线投影获得点击的形状,再对形状进行处理*/
var mouse = new THREE.Vector3();
var raycaster = new THREE.Raycaster();
function onMouseDown(event){
event.preventDefault();
//将屏幕像素坐标转化成camare坐标
mouse.x = (event.clientX/renderer.domElement.clientWidth)*2-1;
mouse.y = - (event.clientY/renderer.domElement.clientHeight)*2+1;
//设置射线的起点是相机
raycaster.setFromCamera( mouse, camera );
//将射线投影到屏幕,如果scene.children里的某个或多个形状相交,则返回这些形状
//第二个参数是设置是否递归,默认是false,也就是不递归。当scene里面添加了Group对象的实例时,就需要设置这个参数为true
//第一个参数不传scene.children也可以,传一个group.children或一个形状数组都可以(这样可以实现一些特别的效果如点击内部的效果)
//另外,因为返回的是一个数组,所以遍历数组就可以获得所有相交的对象,当元素重叠时,特别有用
var intersects = raycaster.intersectObjects(scene.children,true);
if(intersects.length>0){
var currObj = intersects[0].object;
console.log(currObj);
currObj.material.color = new THREE.Color(0x493048);
currObj.position.x += 50;
}
render();
}
注意:如果intersectObjectsobjects : Array, recursive : Boolean)
方法不带 recursive 参数只能检测 Mesh 类型的数组,所以,如果数组里面有Group,请带上recursive 参数。这是在拾取加载进来的对象时非常容易犯的一个错误。
/**
* 旋转参考系
* @param object 需要旋转的对象
* @param axis 旋转轴,是一个向量,new Vetor3(1,0,0)表示绕x轴顺时针旋转,Vetor3(1,0,0)表示绕X轴逆时针旋转
* @param radians 旋转的角度
*/
function rotateAroundWorldAxis(object, axis, radians) {
rotWorldMatrix = new THREE.Matrix4();
rotWorldMatrix.makeRotationAxis(axis.normalize(), radians);
// old code for Three.JS pre r54:
// rotWorldMatrix.multiply(object.matrix);
// new code for Three.JS r55+:
rotWorldMatrix.multiply(object.matrix); // pre-multiply
object.matrix = rotWorldMatrix;
// old code for Three.js pre r49:
// object.rotation.getRotationFromMatrix(object.matrix, object.scale);
// old code for Three.js pre r59:
// object.rotation.setEulerFromRotationMatrix(object.matrix);
// code for r59+:
object.rotation.setFromRotationMatrix(object.matrix);
}
物体动的效果可以是物体本身在动,比如,风车的转动;也可以是世界在动,比如,仰望星空,斗转星移。
旋转参考系和旋转物体本身是不一样的:旋转参考系就好像通过东南西北找路,东西南北永远不变;旋转物体就本身就好像通过左手右手找路,随着物体的改变,左右也变了。下面这个例子是朝右转的:
function render(){
renderer.render(scene,camera);
cube.rotation.x = Math.PI
cube.rotation.y += Math.PI*5/ 180;
requestAnimationFrame(render,1000);
}
首先要算出鼠标移动的速度:
function onMouseMove(event){
var xDisp = event.clientX - prevX;
var yDisp = event.clientY - prevY;
//do something
prevX = event.clientX;
prevY = event.clientY;
render();
}
onMouseMove
不停的触发,相当于单位时间内触发一次,所以,单位时间内的位移即是速度。
求出了鼠标的移动的速度,就可以乘以一定的系数转化成形状的转动,具体的系数可以尝试得出。
注意:这样求出的速度并不精确,因为mousemove事件触发的时间间隔并不是恒定的,而是跟机器的实时性能有关,如果要精确速度,请看: 计算鼠标移动的精确速度