导读
1.项目Spike功能点
2.Three.js基本元素简单介绍
3.Three.js做出xy面网格
4.添加静态点元素
5.添加轨迹球控件
6.添加GUI控制元素属性
一、项目Spike功能点
1.建立三维网格坐标系。
2.修改物体的坐标。
3.添加camera的观察对象改变功能(修改camera的位置就好)。
4.添加camera视角切换,远近切换功能。
二、Three.js基本元素简单介绍
Three.js的基本元素包括scene(场景)、camera(相机)和renderer(渲染器)。
scene是一个容器用来保存并跟踪所有需要渲染的物体,一个scene想要显示任何东西需要三个类型的组 件:相机、光源、物体。
camera定义我们能够在scene里看见的内容。camera分为正投影相机(OrthographicCamera)和透视 相机(PerspectiveCamera)两种。
OrthographicCamera的参数包括:fov(视场)、aspect(长宽比)、 near(近面)、far(远面)
PerspectiveCamera的参数包括:left(左边界)、right(右边界)、top(上边界)、bottom(下边 界)、near(近面)、far(远面)
camera.lookAt(new THREE.Vector3(x,y,z))设置相机聚焦位置。
renderer负责计算指定相机角度下浏览器中scene的样子。考虑到CPU资源和功能,一般选择使用Three.js里的WebGLRenderer对象(另外两种renderer对象为Canvas和SVG的渲染器)
三、Three.js做出xy面网格
首先定义renderer、scene、camera, 并在场景中添加光源light。
光源light包括AmbientLight(环境光)、PointLight(点光源)、SpotLight(聚光灯光源)、DirectionalLight(方向光)、HemisphereLight(半球光)、AreaLight(面光源)、LensFlare(镜头眩光)。
let renderer;
let scene;
let camera, light;
function initRenderer() {
renderer = new THREE.WebGLRenderer(); //定义渲染器
renderer.setSize(window.innerWidth, window.innerHeight); //渲染器大小
renderer.setClearColor(0xFFFFFF); //设置renderer背景色
document.body.appendChild(renderer.domElement); //将renderer添加到对应的对象
}
function initScene() {
scene = new THREE.Scene(); //定义场景
}
function initCamera(){
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight,0.1,100000);
camera.position.set(0,0,800); //定义相机位置
camera.up.x = 0;
camera.up.y = 1;
camera.up.z = 0; //定义相机镜头方向
camera.lookAt({ //定义相机焦点位置
x:0,
y:0,
z:0
});
}
function initLight(){
light = new THREE.DirectionalLight(0xFF0000,1.0,0);
light.position.set(100,100,200);
scene.add(light); //向scene中添加光源
}
然后我们开始准备画一个网格,这里的网格我们以线条组来实现而不直接向场景添加一个二维面对象。
首先我们创建一个线段基础材质(LineBasicMaterial)他可以设置线段的颜色、宽度、端点和连接点。
接着新建一个几何对象,定义将使用到的线段颜色,再定义两个三维坐标点作为线条的端点。并将定义的点集和颜色添加到该几何对象中。
let material = new THREE.LineBasicMaterial({vertexColors: THREE.VertexColors});
let frame_geometry = new THREE.Geometry();
let point_color_1 = new THREE.Color(0x999999);
let point_color_2 = new THREE.Color(0xff0000);
let point_1 = new THREE.Vector3(-1000,0,0);
let point_2 = new THREE.Vector3(1000,0,0);
frame_geometry.vertices.push(point_1);
frame_geometry.vertices.push(point_2);
frame_geometry.colors.push(point_color_1,point_color_1);
接下来我们向scene中添加设定好坐标的线条Line,这里我们使用THREE.Line,线段只有顶点不包含任何面。
//添加横向线条
for (let i=-100;i<100;i++){
let line = new THREE.Line(frame_geometry,material,THREE.LineSegments );
line.position.y = i*10;
scene.add(line);
}
//添加纵向线条
for (let i=-100;i<100;i++){
let line = new THREE.Line(frame_geometry,material,THREE.LineSegments );
line.rotation.z = (90 * Math.PI)/180;
line.position.x = i*10;
scene.add(line)
}
添加横纵线条后我们就得到了一个 xy面上的网格,这里我设置的间隔是10,即一格小格是10*10的大小,如果这里取值为一或者更小出现的是一格黑色的面,原因大家都懂。
ps:我在学习的时候百度的关键词是”three.js 网格绘制“,出来的几篇博客中和我这里的代码是基本一样的,但是每一篇的镜头角度是有区别的,加上renderer的添加对象不一样,可能会出现参考那几篇博客无法看见网格的情况,因为他们镜头观察的焦点不在画网格的面上。
画完这个网格后我们得到的网格的x轴和y轴并不能直观的展现,这里我是用类似的方法在x和y轴添加了不同颜色的线段,当然也可以在for循环里判断一下添加不同的颜色。
下面是这部分的完整代码:
function initFrame(){
let material = new THREE.LineBasicMaterial({vertexColors: THREE.VertexColors});
let frame_geometry = new THREE.Geometry();
let point_color_1 = new THREE.Color(0x999999);
let point_color_2 = new THREE.Color(0xff0000);
let point_1 = new THREE.Vector3(-1000,0,0);
let point_2 = new THREE.Vector3(1000,0,0);
frame_geometry.vertices.push(point_1);
frame_geometry.vertices.push(point_2);
frame_geometry.colors.push(point_color_1,point_color_1);
for (let i=-100;i<100;i++){
let line = new THREE.Line(frame_geometry,material,THREE.LineSegments );
line.position.y = i*10;
scene.add(line);
}
for (let i=-100;i<100;i++){
let line = new THREE.Line(frame_geometry,material,THREE.LineSegments );
line.rotation.z = (90 * Math.PI)/180;
line.position.x = i*10;
scene.add(line)
}
let cross_geometry = new THREE.Geometry();
cross_geometry.vertices.push(point_1);
cross_geometry.vertices.push(point_2);
cross_geometry.colors.push(point_color_2,point_color_2);
let cross_lineX = new THREE.Line(cross_geometry,material,THREE.LineSegments );
let cross_lineY = new THREE.Line(cross_geometry,material,THREE.LineSegments );
cross_lineX.position.y = 0;
cross_lineY.rotation.z = (90 * Math.PI)/180;
cross_lineY.position.x = 0;
scene.add(cross_lineX);
scene.add(cross_lineY);
}
最后我们将上面的function挨个执行就能得到想要的效果。
function threeStart(){
initRenderer();
initCamera();
initScene();
initLight();
initFrame();
renderer.clear();
renderer.render(scene, camera);
}
window.onload = threeStart();
四、添加静态点元素
添加一个静态点元素的方法其实跟添加一根线条类似,准确的说在一个scene中添加一个几何图形物体的过程基本都是一样的,即定义一个形状,再定义它的材质最后用Mesh渲染这个物体。
因为是三维物体,所以虽然说添加的是一个点元素,但是其实用到的是添加球体的方法SphereGeometry,这个方法包含的属性如下:
radius:设置球体半径,决定最终网格多大。默认值是50;
widthSegment:指定竖直方向的分段数(段数越多球体越光滑)。默认值是8,最小值是3;
heightSegment:指定水平方向的分段数,默认值是6,最小值是2;
phiStart:指定从x轴的什么地方开始绘制。取值范围是0到2*Pi,默认值是0;
phiLength:指定从phiStart开始画多少。2*Pi是整球。
thetaStart:该属性用来指定从y轴的什么位置开始绘制。取值范围0到Pi。默认是0;
thetaLength: 该属性用来指定从thetaStart开始画多少。Pi是整球,0.5Pi只绘制上半球;
接下来是在表格里添加一个SphereGeometry作为定位用的点,代码如下:
let labelList = [];
function initSphereLabel(){
let sphereGeometry = new THREE.SphereGeometry(4,10,10);
let sphereMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe:true});
let sphereLabel= new THREE.Mesh(sphereGeometry,sphereMaterial);
sphereLabel.position.set(10,5,10);
sphereLabel.name = `label${labelList.length}`;
labelList.push(sphereLabel);
scene.add(sphereLabel);
}
这里我们引入了物体的一个新属性name,name为物体指定了一个名字,名字在调试时很有用,也可以直接用Scene.getChildByName(name)函数来获取指定对象并改变其属性。
至此我们除了scene的remove函数scene最常用的四种函数已经都出现过了,分别为:
Scene.Add(): 在场景中添加物体;
Scene.Remove(): 在场景中移除物体;
Scene.children(): 获取场景子元素列表;
Scene.getChildByName: 利用name属性获取场景中特定元素。
这里我定义了一个labelList数组,一开始是打算用来存储获取到的定位标签的变量。但是在给sphereLabel命名的时候发现如果直接调用Scene.children.length来作为物体名称,会把之前画的线条和光源等都在里面,不方便查看label,所以这里直接用label.list来存储这些对象(因为是边做边写所以不确定这样做会不会有问题),之后还会用于显示这些对象的坐标等属性。
五、添加轨迹球控件
做到这步可能会发现我们看见的确实是网格但是完全看不出哪里有3D的效果,这是因为我们的camera被设置在了z轴上是垂直观察的xy面,所以渲染出来的效果是一个看起来是2d的网格,通过改变相机的位置我们就能够看出这其实是个3d网格,但是每次都需要手动修改camera的位置是比较麻烦的,这里我们引入一个轨迹球控件TrackballControls来实现(所有引入的控件都能在Three.js风格指南的作者的git中找到,作者是的three.js用例代码在github.com/josdirksen/learning-threejs)
PS: 其实一开始我是准备用dat-GUI来做视角切换的,但是感觉轨迹球能更好的体现这是个3d的场景就先介绍轨迹球了。
引入轨迹球的代码如下:
let trackBallControls;
function initTrackBallControls(){
trackBallControls = new THREE.TrackballControls(camera);
trackBallControls.rotateSpeed = 1.0;
trackBallControls.zoomSpeed = 1.0;
trackBallControls.panSpeed = 1.0;
}
接着我们定义一个新的Three.js对象THREE.Clock。THREE.Clock可以用来精确的计算上次调用后经过的时间,或者一个循环耗费的时间。使用时只需要调用getDelta()函数。
let clock = new THREE.Clock();
然后我们修改一下之前的threeStart()函数把渲染的部分抽出来并调用trackballControls.update()函数更新camera的位置,这个函数需要提供子上次update()函数调用以来经过的时间,这样我们真好就可以使用之前定义的clock的getDelta()方法了。需要注意的是我们这里没有直接将1/60s传递给update函数,因为实际上受外部因素影响,帧率可能会有偏差或者波动,为了camera能平缓的移动和旋转我们需要传入精确的时间差。改动后的doRender代码如下:
function doRender(){
let delta = clock.getDelta();
trackBallControls.update(delta);
requestAnimationFrame(doRender); //根据浏览器刷新频率调用doRender()
renderer.render(scene, camera);
}
至此我们就可以看出这个场景的3d效果了,如下图:
六、添加dat-GUI
使用dat-GUI我们可以方便的创建出一个简单的界面组件用来修改代码中的变量,首先我们在代码中定义一个dat.GUI对象。
let gui = new dat.GUI();
然后我们我们添加对应的代码来控制3d小球的位置:
let controls = new function () {
this.labelPositionX = 0;
this.labelPositionY = 0;
this.labelPositionZ = 0;
};
function initDatGUI(){
gui.add(controls, 'labelPositionX', -1000, 1000);
gui.add(controls, 'labelPositionY', -1000, 1000);
gui.add(controls, 'labelPositionZ', 0, 1000);
}
最后在render时设置小球的属性:
function doRender(){
if(labelList.length>0){
labelList[0].position.set(controls.labelPositionX,controls.labelPositionY,controls.labelPositionZ);
}
//let delta = clock.getDelta();
//trackBallControls.update(delta);
requestAnimationFrame(doRender);
renderer.render(scene, camera);
}
需要注意的是,dat-GUI和轨迹球起冲突啦!!!!!!!,初步猜测是因为GUI修改属性时同样会触发控制球,所以如果要添加dat-GUI的时候需要先注释掉控制球。这个问题我研究后会再写一片博客来分析这个问题,当然如果有大神已经解决了这个问题,还请不吝赐教。
问题解决了,dat-GUI和控制球的冲突是因为添加轨迹球后其监听的对象造成的,研究源码后发现其实TrackballControls 有两个参数
THREE.TrackballControls = function ( object, domElement ) {}
第二个参数是监听的事件绑定的对象,如果没有绑定会默认为window。
将初始化轨迹球的函数改为:
function initTrackBallControls(){
trackBallControls = new THREE.TrackballControls(camera, renderer.domElement);
trackBallControls.rotateSpeed = 1.0;
trackBallControls.zoomSpeed = 1.0;
trackBallControls.panSpeed = 1.0;
}
即可解决问题。
最后的效果如下
最后附上完整的js代码:
let renderer,object;
let camera;
let scene;
let light;
let cube;
let trackBallControls;
let clock = new THREE.Clock();
let delta = clock.getDelta();
let gui = new dat.GUI();
let labelList = [];
function initRenderer() {
renderer = new THREE.WebGLRenderer();
renderer.setSize(600, 600);
renderer.setClearColor(0xFFFFFF);
$('#WebGL').append(renderer.domElement);
}
function initCamera(){
camera = new THREE.PerspectiveCamera(45,600/600,0.1,100000);
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 800;
camera.up.x = 0;
camera.up.y = 1;
camera.up.z = 0;
camera.lookAt({
x:0,
y:0,
z:0
});
}
function initScene() {
scene = new THREE.Scene();
}
function initLight(){
light = new THREE.DirectionalLight(0xFF0000,1.0,0);
light.position.set(100,100,200);
scene.add(light);
}
function initFrame(){
let material = new THREE.LineBasicMaterial({vertexColors: THREE.VertexColors});
let frame_geometry = new THREE.Geometry();
let point_color_1 = new THREE.Color(0x999999);
let point_color_2 = new THREE.Color(0xff0000);
let point_1 = new THREE.Vector3(-1000,0,0);
let point_2 = new THREE.Vector3(1000,0,0);
frame_geometry.vertices.push(point_1);
frame_geometry.vertices.push(point_2);
frame_geometry.colors.push(point_color_1,point_color_1);
for (let i=-100;i<100;i++){
let line = new THREE.Line(frame_geometry,material,THREE.LineSegments );
line.position.y = i*10;
scene.add(line);
line = new THREE.Line(frame_geometry,material,THREE.LineSegments );
line.rotation.z = (90 * Math.PI)/180;
line.position.x = i*10;
scene.add(line)
}
let cross_geometry = new THREE.Geometry();
cross_geometry.vertices.push(point_1);
cross_geometry.vertices.push(point_2);
cross_geometry.colors.push(point_color_2,point_color_2);
let cross_lineX = new THREE.Line(cross_geometry,material,THREE.LineSegments );
let cross_lineY = new THREE.Line(cross_geometry,material,THREE.LineSegments );
cross_lineX.position.y = 0;
cross_lineY.rotation.z = (90 * Math.PI)/180;
cross_lineY.position.x = 0;
scene.add(cross_lineX);
scene.add(cross_lineY);
}
function initSphereLabel(){
let sphereGeometry = new THREE.SphereGeometry(4,10,10);
let sphereMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe:true});
let sphereLabel= new THREE.Mesh(sphereGeometry,sphereMaterial);
sphereLabel.position.set(10,5,10);
sphereLabel.name = `label${labelList.length}`;
labelList.push(sphereLabel);
scene.add(sphereLabel);
}
function initTrackBallControls(){
trackBallControls = new THREE.TrackballControls(camera, renderer.domElement);
trackBallControls.rotateSpeed = 1.0;
trackBallControls.zoomSpeed = 1.0;
trackBallControls.panSpeed = 1.0;
}
let controls = new function () {
this.labelPositionX = 0;
this.labelPositionY = 0;
this.labelPositionZ = 0;
};
function initDatGUI(){
gui.add(controls, 'labelPositionX', -1000, 1000);
gui.add(controls, 'labelPositionY', -1000, 1000);
gui.add(controls, 'labelPositionZ', 0, 1000);
}
function doRender(){
if(labelList.length>0){
labelList[0].position.set(controls.labelPositionX,controls.labelPositionY,controls.labelPositionZ);
}
//let delta = clock.getDelta();
trackBallControls.update(delta);
requestAnimationFrame(doRender);
renderer.render(scene, camera);
}
function threeStart(){
initRenderer();
initCamera();
initScene();
initLight();
initFrame();
initTrackBallControls();
initDatGUI();
initSphereLabel();
doRender();
}
window.onload = threeStart();