写在前面:作为一个前端小白菜,在校期间学习过一点点的Three.js,做过一些小demo,想在这里和大家做下分享,顺便记录一下学习的过程。
Three官方文档
WebGL相信大家已经耳熟能详,是OpenGL的一个衍生版本。能够使得我们不需要安装任何插件就可以构建Web 3D场景。但是WebGL本身的语法比较偏向底层,直接使用WebGL的GLSl语言构建3D场景开发难度比较大。于是乎,Three.js对WebGL做了一层封装,并向我们前端开发人员提供了非常友好的API,使得我们前端开发人员也能快速上手,构建3D场景。
本文将要讲述的内容:
相机就像人的眼睛一样,人站在不同位置,抬头或者低头都能够看到不同的景色。相机决定了场景中那个角度的景色会显示出来。
类型 | 名称 | 构造函数 | 参数说明 | 备注 |
---|---|---|---|---|
ArrayCamera | 相机阵列 | ArrayCamera(array:Array) | 很多相机组成的数组 | |
Camera | 相机 | Camera() | 这是camera的基类 | |
CubeCamera | 立方体相机 | CubeCamera( near : Number, far : Number, cubeResolution : Number, options : Object ) | 最近距离,最远距离,设置立方体边缘的长度,保存传递给自动生成的WebGLRenderTargetCube的纹理参数的对象 | 最近距离,最远距离,立方体分辨率 |
OrthographicCamera | 正交相机 | OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number ) | 相机视锥体左,右,顶,下平面,相机视锥体近,相机视锥体远 | |
PerspectiveCamera | 透视相机 | PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number ) | 相机视锥体垂直视野,相机平截头宽高比,相机视锥体近,相机视锥体远 | 视角,视图宽高比,近平面,远平面 |
StereoCamera | 立体照相机 | StereoCamera( ) | 用于‘3D立体影像’或‘视差屏障’等效果 |
正交相机和透视相机的区别
// scene 的可配置参数
scene = new THREE.Scene();
// 可以给场景增加fog雾化效果 第一个参数为雾化颜色,第二为'开始施雾的最小距离',第三为'雾停止计算和应用的最大距离'。注意:这里的'100'不能小于相机中的near,'400'不能大于相机中的far
scene.fog = new THREE.Fog( 0x444466, 100, 400 );
// 设置场景的颜色
scene.background = new THREE.Color( 0x444466 );
// 设置场景中的所有材质
scene.overrideMaterial = this.materialDepth;
// autoUpdate 默认为true。如果设置,则渲染器会检查场景及其对象是否需要矩阵更新的每一帧。如果不是,那么你必须自己维护场景中的所有矩阵。
scene.autoUpdate
渲染器决定了渲染的结果应该画在页面的什么元素上面,并且以怎样的方式来绘制。
var scene = new THREE.Scene(); //场景
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); // 相机
var renderer = new THREE.WebGLRenderer(); // 渲染器
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
虽然Three提供了很多API供我们画出各种各样的几何图形,但是代码来建模效率往往不太高,而且难度比较大。市面上有很多比较成熟的3D建模软件,比如3dmax,Maya,Blender等。
Three这个库同时也提供了很多的loader去加载各种的模型,比如GLTFLoader,OBJLoader,PLYLoader,FBXLoader等等(不要觉得很神奇,其实3D模型本质上就是一些有规则的二进制文件,我们的loader就是按照模型的规则去读取二进制文件,然后加载到我们的场景中)
下面我就以一个.gltf格式的模型为例
function initCamera_Light_Renderer_Stats_Controls() {
//这里等于是在用脚本添加dom节点
container = document.createElement('div');
document.body.appendChild(container);
//camera 相机
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 2000);
camera.position.set(-9.91836908539192, 26.217305658821992, 101.97602220273721);
//controls 控制器
//轨道控制器OrbitControls.js是一个相当神奇的控件,用它可以实现场景用鼠标交互,让场景动起来,控制场景的旋转、平移,缩放
controls = new THREE.OrbitControls(camera);
//场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
//light 光源
var light = new THREE.HemisphereLight(0xffffff, 0x444444);
light.position.set(0, 20, 0);
scene.add(light);
hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
hemiLight.color.setHSL(0.6, 1, 0.6);
hemiLight.groundColor.setHSL(0.095, 1, 0.75);
hemiLight.position.set(0, 50, 0);
scene.add(hemiLight);
// 渲染器
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
initSky();
effectController.azimuth = 0.44;
// stats性能监视器
// 其中FPS表示:图形处理器每秒钟能够刷新几次;MS表示渲染一帧需要的毫秒数;
stats = new Stats();
container.appendChild(stats.dom);
// model 模型
var onProgress = function(xhr) {
if (xhr.lengthComputable) {
var percentComplete = xhr.loaded / xhr.total * 100;
var percent = document.getElementById("percent");
//percent.innerText = Math.round(percentComplete, 2) + '% 已经加载';
}
};
const gltfLoader = new THREE.GLTFLoader();
gltfLoader.setCrossOrigin('*');
gltfLoader.load(
'nit/model.gltf',
(object) => {
object.scene.scale.set(4, 4, 4);
object.scene.rotation.y = (Math.PI / 2) * 1.3;
// 在加载模型后,在canBeSelectedMeshes变量中加入模型的信息
// 射线摄取法 https://blog.csdn.net/ahilll/article/details/84185576
// canBeSelectedMeshes = object.scene.children;
scene.add(object.scene);
},
);
var onError = function(xhr) {};
window.addEventListener('resize', onWindowResize, false);
}
查看效果:
源代码地址
(多提一嘴,由于我们要加载本地的模型数据,那么如果克隆代码之后直接打开html的话会出现跨域的问题,建议在本地启一个http-server服务,或者使用HBUilder编译器打开,Hbuilder会自动帮我们启一个服务)
大概的实现思路是这样的,首先我们假设整个场景外围有一个立方体,在这里立方体里面有很多随机飘动的雪花,每个雪花可以看作一个对象,然后调用Web的RequestAnimationFrame()来使得我们的场景每一秒渲染60次,也就是60帧,从视觉上达到一个很好的效果,然后每次渲染的时候改变我们每一片雪花的位置。核心代码如下
// 初始化雪花
function initContent() {
/* 雪花图片 */
let texture = new THREE.TextureLoader().load('textures/Snow.png');
let geometry = new THREE.Geometry();
let pointsMaterial = new THREE.PointsMaterial({
size: 2,
transparent: true,
opacity: 0.8,
map: texture,
blending: THREE.AdditiveBlending,
sizeAttenuation: true,
depthTest: false
});
let range = 800;
for (let i = 0; i < 15000; i++) {
let vertice = new THREE.Vector3(
Math.random() * range - range / 2,
Math.random() * range * 1.5,
Math.random() * range - range / 2);
/* 纵向移动速度 */
vertice.velocityY = 0.1 + Math.random() / 3;
/* 横向移动速度 */
vertice.velocityX = (Math.random() - 0.5) / 3;
/* 将顶点加入几何 */
geometry.vertices.push(vertice);
}
geometry.center();
points = new THREE.Points(geometry, pointsMaterial);
points.position.y = -30;
scene.add(points);
}
// ...
// 递归调用,requestAnimationFrame每秒会执行60次
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
update();
}
// ...
// 每次重新渲染的时候遍历下雪花 改变位置
function update() {
stats.update();
let vertices = points.geometry.vertices;
vertices.forEach(function(v) {
v.y = v.y - (v.velocityY);
v.x = v.x - (v.velocityX);
if (v.y <= 0) v.y = 60;
if (v.x <= -20 || v.x >= 20)
v.velocityX = v.velocityX * -1;
});
/* 顶点变动之后需要更新,否则无法实现雨滴特效 */
points.geometry.verticesNeedUpdate = true;
}
查看效果:
在Three官方提供的demo中,我看到这样一个案例,觉得很神奇。不管如何转,就像在真实的世界一样.
在线地址
于是乎,去研究了下他的代码,是这样的
scene = new THREE.Scene();
scene.background = new THREE.CubeTextureLoader()
.setPath( 'textures/cube/Park3Med/' )
.load( [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ] );
后来,我才知道,实际这叫做天空盒模型,只需要六张图,分别作为3D场景的六个面,就可以做出这种效果,类似于这样,然后把图片分成6张,上、下、左、右、前、后.
然后,应用到我们的场景中来
scene = new THREE.Scene();
scene.background = new THREE.CubeTextureLoader()
.setPath('https://zaoren.oss-cn-beijing.aliyuncs.com/')
.load(['skyrender0001.png', 'skyrender0004.png',
'skyrender0003.png', 'skyrender0006.png', 'skyrender0005.png', 'skyrender0002.png']);
效果是这样的:
由于目前我们所有的事情都是使用HTML中的Canvas标签来实现的。在通常情况下,在web中我们区分标签主要是通过DOM节点,像在svg我们也可以给对应的标签加上事件,那么Canvas中,我们是如何去区分3D场景中的对象的呢?
由于我们的Canvas就像是一张画布,显然我们需要区分Canvas中的不同内容只能通过坐标来区分。
由于浏览器是一个2d视口,而在里面显示Three.js的内容是3d场景,所以,现在有一个问题就是如何将2d视口的x和y坐标转换成three.js场景中的3d坐标。好在three.js已经有了解决相关问题的方案,那就是THREE.Raycaster射线,用于鼠标拾取(计算出鼠标移过的三维空间中的对象)等等。我们看一张图片:
大概的意思是,我们相机是一个点,我们鼠标点击的位置是一个点,这两个点可以形成一条射线,穿过我们的3D场景,那么在这条射线上的对象,就是被我们点击到的对象(可能会有多个,我们一般只取第一个)
那么我们如何知道这些模型对象的位置信息呢?问的好!
我们使用loader去加载模型完成以后,会有一个回调函数,带出我们模型中所有建筑的点位信息,我们现在这个回调函数里面用变量将其存储起来,在点击之后就可以通过RayCaster算法去计算到底哪个模型被点击了!下面看代码:
// 第一步: 在回调函数中用 canBeSelectedMeshes 变量接受模型的点位信息
function loadGLTF() {
const gltfLoader = new THREE.GLTFLoader();
gltfLoader.setCrossOrigin('*');
gltfLoader.load(
'https://zaoren.oss-cn-beijing.aliyuncs.com/NIT.gltf',
(object) => {
object.scene.scale.set(4, 4, 4);
object.scene.rotation.y = (Math.PI / 2) * 1.3;
// 在加载模型后,在canBeSelectedMeshes变量中加入模型的信息
// 射线摄取法 https://blog.csdn.net/ahilll/article/details/84185576
canBeSelectedMeshes = object.scene.children.filter((item) => {
if (item.name[0] === 'S') {
if (item.type === 'Mesh') {
item.name = getChineseName(item.name);
return item;
} if (item.type === 'Group') {
item.name = getChineseName(item.name);
item.children.forEach((child) => {
child.name = getChineseName(child.name);
});
return item;
}
}
return false;
});
scene.add(object.scene);
// 用来控制动画显隐的
self.setState({
contentVisibility: true,
});
},
);
}
// 二步: 监听点击事件
renderer.domElement.addEventListener('click', handleClick, false);
// 第三步: 使用RayCaster算法进行碰撞检测
function handleClick(e) {
// 获取点击的位置
const coords = tranformMouseCoord(
e.clientX,
e.clientY,
renderer.domElement,
);
// 检测被射线碰撞到的模型
const intersects = getSelectedMeshes(
coords,
camera,
canBeSelectedMeshes,
);
if (intersects.length > 0) {
getDetail(intersects[0].object.name);
}
}
function getSelectedMeshes(coords, camera, Meshes) {
const raycaster = new THREE.Raycaster();
// 可以看到,我们碰撞检测用到的两个参数 1.点击的相对位置 2.相机的位置
raycaster.setFromCamera(coords, camera);
const intersects = raycaster.intersectObjects(Meshes, true);
return intersects;
}
查看效果:
OK,对Three的分享就到这里拉,因为之后的作内容可能和这块没有很大的交集,暂时不会花太多的时间在这一块上,作为刚毕业的前端小白菜,毕业后的头两年还是先打好基础,哈哈。