本文的程序实现了加载外部 stl 格式的模型, 以及学习了如何把加载的模型变为一个粒子系统, 并使用 Tween.JS 对该粒子系统进行动画设置
模型动画 demo 演示(网页加载速度可能会比较慢)
demo 地址: https://nsytsqdtn.github.io/demo/naval_craft/naval_craft
demo 截图如下:
原模型截图:
在我们写 three.JS 的网页的时候, 大多时候并不需要我们去手动建立模型, 一些复杂的模型都是通过建模软件去完成, 所以在这里去学习如何去将外部的模型加载到我们的网页中来.
three.JS 支持导入的模型有很多, 包括我们常见的 OBJ,FBX,STL,PLY,JSON 等等格式, 在这个程序中, 我选择了使用 STL 模型来进行学习.
.stl 文件是在计算机图形应用系统中, 用于表示三角形网格的一种文件格式, 常用于 3d 打印技术使用, 因为 STL 格式的文件在网上可以免费不用注册的下载, 比较方便. 这里推荐一个还不错的网站, http://www.3dhoo.com/model , 里面有很多免费直接下载 STL 格式的模型.
加载外部模型
在 three.JS 中, 我们要加载外部模型, 就需要引入相应的 JS 文件. 比如我需要引入 STL 格式的文件, 我就引入 "three.js\examples\js\loaders\STLLoader.js", 其他格式的 JS 文件在 loaders 文件夹也都能找到, 如果是 three.JS 没有支持导入的模型格式, 就需要自己写一个加载器, 网上也有许多的教程.
引入相应 JS 文件以后, 我们首先要做的事创建一个加载器.
let loader = new THREE.STLLoader();// 创建 stl 的加载器, 用加载器来加载 stl 模型
我们要使用该加载器加载模型, 就需要调用 loader .load(filename,onSuccess(bufferGeometry),onProgress(xhr),onError(error))这个方法
其中:
filename 是模型的路径
onSuccess(bufferGeometry)是加载成功后回调处理(参数为生成的模型的几何体),
注意: 这里的几何体不是我们常用的 geometry, 而是 bufferGeometry, 它和 geometry 还是有一些的区别, 但是也都可以作为 THREE.Mesh()的第一个参数穿进去. 具体可以进行百度.
onProgress(xhr)是加载过程中回调处理(xhr 对象属性可计算出已完成加载百分比)
onError(error)是失败回调处理方法
一般我们只需要使用前两个参数就可以完成工作.
- let loader = new THREE.STLLoader();// 创建 stl 的加载器, 用加载器来加载 stl 模型
- let loader.load("../../../asset/ship.stl", function (bufferGeometry) {
- // 加载模型的方法,
- // 第一个参数是模型的路径, 第二个参数时候我们定义的回调函数, 一旦模型加载成功, 回调函数就会被调用
- let material = new THREE.MeshBasicMaterial();
- let mesh = new THREE.Mesh(bufferGeometry,material);
- scene.add(mesh);
- }
一般只需要这样写回调函数, 模型就可以成功加载.
但我在这里想根据该模型去创建一个粒子系统, 像本文开头的那样, 所以我们需要改一下代码.
- let loader = new THREE.STLLoader();// 创建 stl 的加载器, 用加载器来加载 stl 模型
- group = new THREE.Object3D();
- loader.load("../../../asset/ship.stl", function (bufferGeometry) {// 加载模型的方法, 第一个参数是模型的路径, 第二个参数时候我们定义的回调函数, 一旦模型加载成功, 回调函数就会被调用
- let geometry = new THREE.Geometry().fromBufferGeometry(bufferGeometry);//stl 模型加载到 JS 里就会变成 bufferGeometry 类型, 我们先用一个方法把它变成 Geometry 类型
- loadGeometry = geometry.clone();// 创建该 geometry 的克隆体, 后面会用到
- let material = new THREE.PointsMaterial({// 点云的材质
- color: 0xffffff,
- transparent: true,
- opacity: 1,
- size: 0.5,// 可自由修改看看效果
- blending: THREE.AdditiveBlending,
- map: generateSprite()// 自定义画布图案来充当每一个粒子的材质
- });
- // 创建点云, 以及设置它的位置及旋转角度, 调整到最好看的地方
- group = new THREE.Points(geometry, material);
- group.sortParticles = true;
- group.position.set(0,0,0);
- group.position.x -=70;
- group.rotation.x = Math.PI*3/2;
其中:
我们使用 THREE.Geometry().fromBufferGeometry(bufferGeometry)函数把 bufferGeometry 类型改为 geometry 类型, 因为该类型我们更加熟悉, 后面使用起来也比较方便.
generateSprite()函数是在之前的文章也介绍过的, 创建一个颜色渐变的画布, 来充当粒子系统纹理, 这里就不再赘述了. 具体代码如下:
- // 自定义渐变颜色的画布, 前面的文章有介绍, 这个方法在写 three.JS 程序很常用
- function generateSprite() {
- var canvas = document.createElement('canvas');
- canvas.width = 16;
- canvas.height = 16;
- var context = canvas.getContext('2d');
- var gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);
- gradient.addColorStop(0, 'rgba(255,255,255,1)');
- gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
- gradient.addColorStop(0.4, 'rgba(0,0,255,1)');
- gradient.addColorStop(1, 'rgba(0,0,0,1)');
- context.fillStyle = gradient;
- context.fillRect(0, 0, canvas.width, canvas.height);
- var texture = new THREE.Texture(canvas);
- texture.needsUpdate = true;
- return texture;
- }
Tween.JS 动画
tweenjs 是使用 JavaScript 中的一个简单的补间动画库, 支持数字, 对象的属性和 CSS 样式属性的赋值.
tweenjs 以平滑的方式修改元素的属性值, 需要传递给 tween 要修改的值, 动画结束时的最终值和动画花费时间, 之后 tween 引擎就可以计算从开始动画点到结束动画点之间值, 从而产生平滑的动画效果.
我们首先需要引入 tween.JS 文件, 该文件的路径是 "three.js\examples\js\libs\tween.min.js", 也可以直接百度搜索 tween.JS 去下载.
具体的用法是:
- let posSrc = {
- pos: 0
- };// 创建一个 posSrc 的对象, 该对象里面有 pos 的属性, 并初始化该属性为 0
- let tween = new TWEEN.Tween(posSrc).to({
- pos: 1
- }, 5000);// 创建 tween 的补间动画, 使 posSrc 中的 pos 属性的值在 5000ms 内从 0 到 1 变化
- tween.easing(TWEEN.Easing.Sinusoidal.InOut);// 配置缓动效果
我们创建了 TWEEN.Tween 对象, 这个对象会确保 x 属性值在 5000 毫秒内从 0 变化到 1. 通过 Tweenjs, 你还可以指定属性值是如何变化的, 是线性的, 指数性的, 还是其他任何可能的方式. 属性值在指定时间内的变化被称为 easing(缓动), 在 Tween.JS 中你可以使用 easing()方法来配置缓动效果. 我们还可以创建更多的 TWEEN.Tween 对象, 并使用 chain(TWEEN.Tween)函数链接多个补间动画.
我们还需要一个 update 的函数, 在每次更新补间的时候, 都可以去更新每个粒子的位置, 来实现的动画效果.
- let posSrc = {pos: 0};// 创建一个 posSrc 的对象, 该对象里面有 pos 的属性
- // 并初始化该属性为 0
- let tween = new TWEEN.Tween(posSrc).to({pos: 1}, 5000);// 创建 tween 的补间动画
- // 使 posSrc 中的 pos 属性的值在 5000ms 内从 0 到 1 变化
- tween.easing(TWEEN.Easing.Sinusoidal.InOut);// 配置缓动效果
- let tweenStand = new TWEEN.Tween(posSrc).to({pos: 1}, 2000);// 让动画在 pos 的值
- // 变为 1 后停止一段时间, 方便我们观察, 所以再创建一个 tween, 让 pos 从 1 到 1(即不变)
- tween.easing(TWEEN.Easing.Sinusoidal.InOut);// 配置缓动效果
- let tweenBack = new TWEEN.Tween(posSrc).to({pos: 0}, 5000);// 创建 tweenBack 的
- // 补间动画, 和初始相反, 使 posSrc 中的 pos 属性的值在 5000ms 内从 1 到 0 变化
- tweenBack.easing(TWEEN.Easing.Sinusoidal.InOut);// 配置缓动效果
- // 每一个补间动画之间使用 chain()连接起来
- tween.chain(tweenStand);
- tweenStand.chain(tweenBack);
- tweenBack.chain(tween);
- // 在补间的过程中, 让所有的粒子开始移动
- let onUpdate = function () {
- let pos = posSrc.pos;// 定义一个 pos, 赋值为 posSrc 对象的 pos 属性
- let count = 0;
- loadGeometry.vertices.forEach(function (e) {// 遍历每个顶点
- // 这里需要遍历刚刚克隆的 geometry
- //(暂时不是很明白这点, 反正如果遍历 group.geometry.vertices
- // 动画系统会让整个物体一起移动, 没有伸展开来的效果).
- var newZ = e.z * pos;// 得到新的 Z 值, 根据当前的 pos 值去改变
- group.geometry.vertices[count++].set(e.x, e.y, newZ);// 设置每个顶点的位置
- //group.geometry.vertices 是数组类型, 所以用 count 作为索引
- group.geometry.verticesNeedUpdate = true;// 重要, 不然会没有动画效果
- });
- group.sortParticles = true;
- };
- //tween 在每次更新后会执行 tween.onUpdate()函数
- // 里面的参数就是我们自定义要让它如果去运动的函数, 即上面写的 onUpdate
- tween.onUpdate(onUpdate);
- tweenStand.onUpdate(onUpdate);
- tweenBack.onUpdate(onUpdate);
- tween.start();// 开启 tween
在这段代码中, 我们创建了三个个补间: tween,tweenStand,tweenBack. 第一个补间定义了 position 属性如何从 1 过渡到 0, 第三个刚好相反, 第二个是让动画暂时停下. 通过 chain(方法可以将这三个补间衔接起来, 这样当动画启动之后, 程序就会在这三个补间循环. 代码最后定义的是 onUpdate()方法, 这个方法遍历粒子系统中的所有顶点, 并使用补间 (this.pos) 提供的位置更新顶点的位置.
补间动画需要在模型加载完成后就启动, 所以我们在下面的函数末尾调用 tween.start()方法:
如果之前没有把 bufferGeometry 转化为 Geometry 类型, 要去更改每个顶点的位置会变得比较麻烦.
最后还需要告知 three.JS 什么时候刷新所有的补间动画, 所以在 render()函数里加上 TWEEN.update();
- function render() {
- TWEEN.update();// 通知 TWEEN 在什么时候去刷新补间动画, 重要, 否则会没有动画
- // 性能监控器的更新
- stats.update();
- renderer.clear();
- requestAnimationFrame(render);
- renderer.render(scene, camera);
- }
到了这里, 程序的大体就已经完成, 剩下的就是创建场景, 摄像机, 渲染器等等东西以及调整模型的位置. 这里不再赘述.
完整的代码如下:
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <title>
- Naval Craft Sprite
- </title>
- <script src="../../import/three.js">
- </script>
- <script src="../../import/stats.js">
- </script>
- <script src="../../import/Setting.js">
- </script>
- <script src="../../import/OrbitControls.js">
- </script>
- <script src="../../import/tween.min.js">
- </script>
- <script src="../../import/STLLoader.js">
- </script>
- <style type="text/css">
- div#WebGL-output { border: none; cursor: pointer; width: 100%; height:
- 850px; background-color: #333333; }
- </style>
- </head>
- <body onload="threeStart()">
- <div id="WebGL-output">
- </div>
- <script>
- let camera,
- renderer,
- scene,
- controller;
- function initThree() {
- // 渲染器初始化
- renderer = new THREE.WebGLRenderer({
- antialias: true // 抗锯齿开启
- });
- // 设置渲染的大小
- renderer.setSize(Windows.innerWidth, Windows.innerHeight);
- // 设置渲染的颜色
- renderer.setClearColor(0x333333);
- renderer.shadowMapEnabled = true; // 开启阴影的渲染
- renderer.shadowMapType = THREE.PCFSoftShadowMap; // 设置阴影类型为柔和
- document.getElementById("WebGL-output").appendChild(renderer.domElement); // 将渲染添加到 div 中
- // 初始化摄像机, 这里使用透视投影摄像机
- camera = new THREE.PerspectiveCamera(50, Windows.innerWidth / Windows.innerHeight, 0.01, 10000);
- camera.position.set(35, 35, 75); // 相机的位置, 自由调整
- camera.up.x = 0; // 设置摄像机的上方向为哪个方向, 这里定义摄像的上方为 Y 轴正方向
- camera.up.y = 1;
- camera.up.z = 0;
- // 摄像机对准的地方
- camera.lookAt(0, 0, 0);
- // 初始化场景
- scene = new THREE.Scene();
- // 相机的移动
- controller = new THREE.OrbitControls(camera, renderer.domElement);
- // 相机围绕旋转的目标, 设置为原点
- controller.target = new THREE.Vector3(0, 0, 0);
- }
- let loadGeometry;
- let group;
- function initObject() {
- let loader = new THREE.STLLoader(); // 创建 stl 的加载器, 用加载器来加载 stl 模型
- group = new THREE.Object3D();
- loader.load("../../asset/naval_craft.stl",
- function(bufferGeometry) { // 加载模型的方法, 第一个参数是模型的路径, 第二个参数时候我们定义的回调函数, 一旦模型加载成功, 回调函数就会被调用
- let geometry = new THREE.Geometry().fromBufferGeometry(bufferGeometry); //stl 模型加载到 JS 里就会变成 bufferGeometry 类型, 我们先用一个方法把它变成 Geometry 类型
- loadGeometry = geometry.clone(); // 创建该 geometry 的克隆体, 后面会用到
- let material = new THREE.PointsMaterial({ // 点云的材质
- color: 0xffffff,
- transparent: true,
- opacity: 1,
- size: 0.5,
- // 可自由修改看看效果
- blending: THREE.AdditiveBlending,
- map: generateSprite() // 自定义画布图案来充当每一个粒子的材质
- });
- // 创建点云, 以及设置它的位置及旋转角度, 调整到最好看的地方
- group = new THREE.Points(geometry, material);
- group.sortParticles = true;
- group.position.set(0, 0, 0);
- group.position.x -= 70;
- group.rotation.x = Math.PI * 3 / 2;
- let posSrc = {
- pos: 0
- }; // 创建一个 posSrc 的对象, 该对象里面有 pos 的属性, 并初始化该属性为 0
- let tween = new TWEEN.Tween(posSrc).to({
- pos: 1
- },
- 5000); // 创建 tween 的补间动画, 使 posSrc 中的 pos 属性的值在 5000ms 内从 0 到 1 变化
- tween.easing(TWEEN.Easing.Sinusoidal.InOut); // 配置缓动效果
- let tweenStand = new TWEEN.Tween(posSrc).to({
- pos: 1
- },
- 2000); // 让动画在 pos 的值变为 1 后停止一段时间, 方便我们观察, 所以再创建一个 tween, 让 pos 从 1 到 1(即不变)
- tween.easing(TWEEN.Easing.Sinusoidal.InOut); // 配置缓动效果
- let tweenBack = new TWEEN.Tween(posSrc).to({
- pos: 0
- },
- 5000); // 创建 tweenBack 的补间动画, 和初始相反, 使 posSrc 中的 pos 属性的值在 5000ms 内从 1 到 0 变化
- tweenBack.easing(TWEEN.Easing.Sinusoidal.InOut); // 配置缓动效果
- // 每一个补间动画之间使用 chain()连接起来
- tween.chain(tweenStand);
- tweenStand.chain(tweenBack);
- tweenBack.chain(tween);
- // 在补间的过程中, 让所有的粒子开始移动
- let onUpdate = function() {
- let pos = posSrc.pos; // 定义一个 pos, 赋值为 posSrc 对象的 pos 属性
- let count = 0;
- loadGeometry.vertices.forEach(function(e) { // 遍历每个顶点, 这里需要遍历刚刚克隆的 geometry
- var newZ = e.z * pos; // 得到新的 Z 值, 根据当前的 pos 值去改变
- group.geometry.vertices[count++].set(e.x, e.y, newZ); // 设置每个顶点的位置, group.geometry.vertices 是数组类型, 所以用 count 作为索引
- group.geometry.verticesNeedUpdate = true; // 重要, 不然会没有动画效果
- });
- group.sortParticles = true;
- };
- //tween 在每次更新后会执行 tween.onUpdate()函数, 里面的参数就是我们自定义要让它如果去运动的函数, 即上面写的 onUpdate
- tween.onUpdate(onUpdate);
- tweenStand.onUpdate(onUpdate);
- tweenBack.onUpdate(onUpdate);
- tween.start(); // 开启 tween
- scene.add(group);
- });
- }
- // 自定义渐变颜色的画布, 前面的文章有介绍, 这个方法在写 three.JS 程序很常用
- function generateSprite() {
- var canvas = document.createElement('canvas');
- canvas.width = 16;
- canvas.height = 16;
- var context = canvas.getContext('2d');
- var gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);
- gradient.addColorStop(0, 'rgba(255,255,255,1)');
- gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
- gradient.addColorStop(0.4, 'rgba(0,0,255,1)');
- gradient.addColorStop(1, 'rgba(0,0,0,1)');
- context.fillStyle = gradient;
- context.fillRect(0, 0, canvas.width, canvas.height);
- var texture = new THREE.Texture(canvas);
- texture.needsUpdate = true;
- return texture;
- }
- // 渲染函数
- function render() {
- TWEEN.update(); // 通知 TWEEN 在什么时候去刷新补间动画, 重要, 否则会没有动画
- // 性能监控器的更新
- stats.update();
- renderer.clear();
- requestAnimationFrame(render);
- renderer.render(scene, camera);
- }
- // 功能函数
- function setting() {
- loadFullScreen();
- loadAutoScreen(camera, renderer);
- loadStats();
- }
- // 运行主函数
- function threeStart() {
- initThree();
- initObject();
- setting();
- render();
- }
- </script>
- </body>
- </HTML>
来源: https://www.cnblogs.com/nsytsqdtn/p/10850548.html