threejs之前端开飞机✈️

初识threejs

最近在项目中遇到一些需要threejs的知识,特此撰写一篇相关文档记录一下自己的threejs学习。

threejs官方文档地址官方文档

基本概念

创建一个threejs首先最基本的需要场景scene、相机camera、渲染器render和容器DOM元素。

[图片上传失败...(image-5e56d6-1682914437522)]

场景 Scene

const scene = new THREE.Scene();

场景能够让你在什么地方、摆放什么东西来交给three.js来渲染,这是你放置物体、灯光和摄像机的地方。

他的一些基本属性都可以在threejs的官方文档库中查阅到。

相机Camera

相机一共分为两类:正投影相机和透视相机

  • 正投影相机是一种平行投影形式的相机,它将场景投影到一个平面上。这种相机通常用于2D场景或需要物体尺寸相对固定的3D场景。正投影相机没有远近裁剪面,并且不会根据距离来改变物体的大小,因此在使用正投影相机拍摄3D场景时需要注意物体大小的调整。

  • 透视相机则是一种模拟人眼视角的相机,其投影效果更贴近实际情况。透视相机可以通过设置视角(Field of View)、近裁剪面(Near Clipping Plane)和远裁剪面(Far Clipping Plane)来控制场景的视野和深度感。透视相机投影出来的图像可以根据距离的远近来改变物体的大小,这也符合我们在现实世界中观察物体的规律。

官方两个相机视角的example

  1. 第一类是PerspectiveCamera(透视摄像机)

const camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );

[图片上传失败...(image-67f49c-1682914437522)]

其中4个参数的含义分别是

  • fov — 摄像机视锥体垂直视野角度(越大边缘畸变越严重)
  • aspect — 摄像机视锥体长宽比
  • near — 摄像机视锥体近端面
  • far — 摄像机视锥体远端面

这些参数一起定义了摄像机的viewing frustum(视锥体)。

  1. 第二类是 OrthographicCamera(正交摄像机)

const camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );

OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number )

其中6个参数的含义分别是

left — 摄像机视锥体左侧面。
right — 摄像机视锥体右侧面。
top — 摄像机视锥体上侧面。
bottom — 摄像机视锥体下侧面。
near — 摄像机视锥体近端面。
far — 摄像机视锥体远端面。

这些参数一起定义了摄像机的viewing frustum(视锥体)。

渲染器Renderer

渲染器是一个用于将场景和相机输出成图像的组件。它实现了WebGL的接口,并提供了简单易用的API来创建3D图形。它负责将场景、相机和材质等元素渲染到屏幕上。

在Three.js中,有几种不同类型的渲染器可供选择,例如WebGLRenderer、WebGLRenderTarget等。其中最常用的是WebGLRenderer,它使用WebGL技术进行渲染,并且可以自动适应不同的设备和浏览器。使用WebGLRenderer时,可以设置一些参数,如清空颜色、渲染目标的大小、是否启用阴影等。同时还可以设置多个渲染目标,以支持后期处理等高级功能。

demo实践

初始化

首先创建一个Vue3项目

Vue create [项目名称]

引入threejs包

npm install three

template中创建div作为DOM元素放置threejs对象

<template>
    <div class="scene-container" ref="canvasRef"></div>
</template

script中引入threejs并且创建场景、相机、渲染器三要素

<script lang="ts" setup>
import * as THREE from 'three';
let camera:any;
let scene:any;
let renderer:any;
</script>

实例化场景scene,相机camera

scene = new THREE.Scene();
//使用透视相机,具体参数上面有介绍
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 650, 0);//设置相机位置

添加立方纹理背景,这里的png图片要注意格式,必须是正方形图片

不想找图片的话,图片资源在官方example的github中有,可以自行提取。

const cubeLoader = new THREE.CubeTextureLoader();
    cubeLoader.setPath('/img/');
    const textureCube = cubeLoader.load([
        'px.png', 'nx.png',
        'py.png', 'ny.png',
        'pz.png', 'nz.png'
    ]);
scene.background = textureCube;

增加光源

//增加环境光源
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(ambientLight);
//增加点光源
pointLight = new THREE.PointLight(0xffffff, 5);
pointLight.position.set(-500, 1000, 100);
scene.add(pointLight);

增加一个平台底座,盛放等等加载的模型

//添加三位平台底座
scene.add(new THREE.GridHelper(800, 20));

接着把所有的东西都交给渲染器渲染出来

// 渲染
renderer = new THREE.WebGLRenderer({ antialias: true });//antialias - 是否执行抗锯齿。默认为false.
renderer.setPixelRatio(window.devicePixelRatio);//设置设备像素比。通常用于避免HiDPI设备上绘图模糊
renderer.setSize((window.innerWidth - 292), (window.innerHeight - 54));
const container: any = canvasRef.value
container.appendChild(renderer.domElement);

这个时候你是能加载出背景了,但是你无法转动方向,这时候就要介绍轨道控制器OrbitControls

[图片上传失败...(image-409934-1682914437522)]

OrbitControls 是一个附加组件,必须显式导入。
在 THREE.js 中,有两种不同版本的 OrbitControls 控制器。第一种是较早版本的 OrbitControl,它通常存储在 addons 文件夹中,文件路径为 three/addons/controls/OrbitControls.js。而第二种是更新的 OrbitControls,它位于 examples 文件夹中,文件路径为 three/examples/jsm/controls/OrbitControls.js

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';//推荐使用新版本的

const controls = new OrbitControls(camera, renderer.domElement);
controls.enabled = false; // 关闭用户控制相机视角权限
controls.minDistance = 300;
controls.maxDistance = 700;
window.addEventListener('resize', onWindowResize);

/**
 * 画面大小
 */
function onWindowResize() {
    camera.aspect = (window.innerWidth / window.innerHeight);
    camera.updateProjectionMatrix();//更新摄像机投影矩阵。在任何参数被改变以后必须被调用。
    renderer.setSize(window.innerWidth, window.innerHeight);
}
//注意,在大多数属性发生改变之后,你将需要调用.updateProjectionMatrix来使得这些改变生效。

至此,你就可以看到一个带有平面网格平台的可控制画面角度的三维画面了!

[图片上传失败...(image-2bb070-1682914437522)]

加载模型

现在我们可以在里面整点活,加载一些有趣的模型

我的模型资源是在这个gltf模型网站上下载的,下载好模型后,就是要添加到场景中了!

//额外引入gltf包
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// 引入机器人
const planeLoader = new GLTFLoader();
planeLoader.load('/models/plane.gltf', (gltf: any) => {
    const plane = gltf.scene;
    const mixer = new THREE.AnimationMixer(plane) //配合动画使用,稍作解释
    plane.scale.set(20, 20, 20); // 调整大小
    plane.position.set(0, 0, 0); // 调整位置
    scene.add(plane) // 添加
});

一架b52就被我偷回了家哈哈哈

[图片上传失败...(image-b6cb8b-1682914437522)]

光有一个飞机好像还少点什么,加一点树木点缀一下。

const treeLoader = new GLTFLoader();
let treeGroup;//放置多个树木可以一次性加入到组中
treeLoader.load('/models/tree.gltf', (gltf: any) => {
    const tree = gltf.scene;
    // 复制树木模型
    for (let i = 0; i < 50; i++) {
        tree.scale.set(10, 10, 10)
        const clonedTree = tree.clone(); // 复制树木模型
        clonedTree.position.set(-200 + Math.random() * 400, 0, -200 + Math.random() * 400); // 随机生成位置
        treeGroup.add(clonedTree); // 添加到组中
    }
  // 加载模型
  treeGroup = new THREE.Group();
  scene.add(treeGroup);
});

[图片上传失败...(image-42f2f9-1682914437522)]

这样树也有了,不过b52好像被树遮住了,那么干脆就让它起飞🛫️,让它动起来!

模型动画

这里就用到了刚刚在plane中定义的动画混合器AnimationMixer,首先我们了解一下它。

动画混合器是用于场景中特定对象的动画的播放器。当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器。

首先我们创建一些点,然后将点使用CatmullRomCurve3连接成线,用于作为飞机的飞行路线。

// 创建曲线
const points = [
    new THREE.Vector3(300, 110, -200),
    new THREE.Vector3(300, 500, 200),
    new THREE.Vector3(-300, 10, 200),
    new THREE.Vector3(-300, 250, -200),
];

// 绘制曲线
const curve = new THREE.CatmullRomCurve3(points, true, 'catmullrom', 0.5);
const path = new THREE.Path(curve.getPoints(200)); // 将曲线转换成路径

介绍一下这里的点位坐标,在threejs中点位坐标是下面这种形式,而(0,0,0)坐标中心就是网格平台的中心点。

[图片上传失败...(image-57e963-1682914437522)]

接着介绍一下CatmullRomCurve3

使用Catmull-Rom算法, 从一系列的点创建一条平滑的三维样条曲线。

它提供的几个参数可以了解一下

Vector3点数组
closed – 该曲线是否闭合,默认值为false。
curveType – 曲线的类型,默认值为centripetal
tension – 曲线的张力,默认为0.5

接着我们下面将创建好的曲线显现出来,便于我们对飞行路线做调试,以免撞到地面🫣

// 曲线轨迹 测试使用
const geometry = new THREE.BufferGeometry().setFromPoints(curve.getPoints(50));
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
const curveObject = new THREE.Line(geometry, material);
scene.add(curveObject)

接着你就可以看到,绘制的飞行曲线了!我已经迫不及待想看到她飞起来的样子!

[图片上传失败...(image-e00083-1682914437522)]
接下来就该使用AnimationMixer将b52起飞!我们创建一个Object3D基类,将飞机作为子元素添加进去。并且设置他的位置等。

在上方绘制曲线的时候,获取曲线全场curveLength,然后通过定制一个速度,得到完成飞行全距离的总时间。在开始时定义一个startTime,然后将定义的moveTime取余,占全长的多少即位该移动的距离

function palyAnimate() {
  if (plane && mixer) {
    // 创建Follower
    let follower = new THREE.Object3D();
    scene.add(follower);
    const path = new THREE.Path(curve.getPoints(200)); // 将曲线转换成路径
    follower.position.copy(path.getPointAt(0));// 将follower的位置设置在初始位置
    follower.lookAt(path.getPointAt(0.1)); // 设定为朝向曲线路径的第二个点的方向
    scene.remove(plane);// 删除原来的模型
    follower.add(plane);// 将模型作为子元素添加到follower中 

    const totalTime = curveLength / 1 // 这里的1是速度
    startTime += 1;
    const moveTime = startTime % totalTime;
    const distance = (moveTime * 10) % curveLength; //应该移动的距离
    const planePosition = curve.getPointAt(distance / curveLength)//获取follower应该在曲线上的位置
    follower.position.copy(planePosition); // 更新follower的位置
    const lookAhead = 0.1; // 指定follower朝向曲线路径前方的距离(可根据需要调整)
    const target = curve.getPointAt((distance + lookAhead) / curveLength); // 获取follower朝向的目标点
    follower.lookAt(target); // 更新follower的朝向
        }
    requestAnimationFrame(palyAnimate);
    renderer.render(scene, camera);
}

最后调整一下曲线和模型大小就可以看到b52在空中翱翔!

2023-04-26 23.13.35.gif

最后在加上一些相机的运镜视角

//调整相机、光线角度
function rotateCamera() {
    theta += 0.1;
    camera.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta));
    camera.position.y = Math.abs(radius * Math.sin(THREE.MathUtils.degToRad(theta))) + 100;
    camera.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta));
    pointLight.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta))
    pointLight.position.y = radius * Math.sin(THREE.MathUtils.degToRad(theta));
    pointLight.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta));
    camera.lookAt(scene.position);
    controls.update(); // 更新OrbitControls
}

[图片上传失败...(image-727fcb-1682914437522)]

最终效果还是可以看看的,也顺利的让飞机起飞了!

初识threejs可能还有很多不对的地方,希望大家多多包涵😃

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容