three.js官网:three.js docs
中文技术文档1:| 麒跃科技
中文技术文档2:3. 开发和学习环境,引入threejs | Three.js中文网
很多教程一开始要大家自己部署three.js的中文本地部署,我就不弄了,我弄了半天也没弄出来烦了,反正我也不爱看官方文档我就不弄了,直接开干。
另外本人学的前端,习惯用vue就以vue的开发框架做基础引入three.js了
一、基础部署
1、先创建新文件夹,然后终端打开,按以下步骤一步一步输入命令(别管为什么,我没了解)
npm init vite@latest
然后写你的项目名称,随便就行
选择vue
选JS
然后根据最后的提示把这三句执行
然后成功搭建vue的框架
最后再导入three.js就好了
二、先尝试一个vue的小demo
先把app.vue的js、html、css部分先删去,然后用app.vue的全局样式来展示three.js的效果
1、样式,先初始化,因为three.js是基于canva画布来成像的,所以要设置一下canvas样式,先不用管那么多直接复制就好
<style>
*{
margin: 0;
padding: 0;
}
canvas{
width: 100vw;
height: 100vh;
display: block;
position: fixed;
top: 0;
left: 0;
}
</style>
2、js部分
首先前面我们已经下载了three.js,那么就要在js部分导入
// 导入three.js
import * as THREE from "three"
然后要呈现下面这种基本的three3D场景,首先要创建一个【场景】和一个【相机】,你就创就行了别问那么多
创建场景
//创建场景
const scene = new THREE.Scene();
创建相机
这里需要配置这么几个参数:
//创建相机
const Camera = new THREE.PerspectiveCamera(
45, //相机视角
window.innerWidth / window.innerHeight, //宽高比
0.1, //近平面,最近能看到多近
1000 //远平面,最远能看多远
);
这里的视角可以根据下面的图来理解
创建渲染器
要有渲染器才能把场景渲染到屏幕
//创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth , window.innerHeight);//渲染器的大小就是屏幕的大小
document.body.appendChild(renderer.domElement);//这样就是是把canvas添加进去
现在还是一片空白
那么现在就加一个【几何体】看看,并设置它的【材质】(暂时用绿色)
//创建几何体
const geometry = new THREE.BoxGeometry(1,1,1);
//设置材质
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
还得把【几何体】、【材质】给装进一个【网格(或者叫立方)】里,然后往场景里填加这个【网格】
// 创建网格
const cube = new THREE.Mesh(geometry, material);
//将网格添加到场景
scene.add(cube)
然后初始化相机的位置
//设置相机位置
camera.position.z = 5
camera.lookAt(0,0,0)
现在还不能看到方块、场景,因为还没有渲染,这里就需要调用渲染器的render函数来进行渲染,把我们的【场景】和【相机】放进去
//渲染
renderer.render(scene,camera);
但是现在只能看到一个静态的一面的正方形,想看动起来的正方体就要调用动画函数
创建一个动画函数,然后调用requestAnimationFrame();函数来实现补帧,比如你的函数名叫xxx,那么requestAnimationFrame(xxx);就是自动来调用你这个xxx函数进行动画补帧,然后旋转的逻辑就是这三:cube.rotation.x、cube.rotation.y、cube.rotation.z,让它们不停自增就可以达到旋转了(其实选两个就够了)
最后别忘了:1、把渲染函数放进动画函数里 2、在下面调用动画函数
//渲染函数
function animate(){
requestAnimationFrame(animate);
//旋转
cube.rotation.x += 0.01
cube.rotation.y += 0.01
//渲染
renderer.render(scene,camera);
}
animate();
然后就爽了
三、开始正式认识3D
1、三维坐标系
这是啥我就不讲了,学过空间几何的都懂,那么像我们刚刚创建的场景里,只看到乌漆嘛黑的背景跟一个烂盒子在那转,根本没办法分清方向啊
【那么我们就要在【场景】里添加一个xyz轴辅助线】
//添加世界坐标辅助器
const axesHelpr = new THREE.AxesHelper(5) //5这个参数是指xyz轴的线段要多长
scene.add(axesHelpr)
红色X轴、绿色Y轴、蓝色Z轴,但是此时我们看不到Z轴是因为我们一开始设置了初始化相机的视角是正对着Z轴的,所以Z轴就是一个点
那么我们可以把相机位置偏移一点
但是光这么看不好操控,我们想要拖动坐标轴,或者说改变我们的视角方向怎么办
【创建轨道控制器】
先导入控制器
然后添加轨道控制器
//添加轨道控制器
const controls = new OrbitControls(camera,renderer.domElement);
还要再动画函数里更新轨道控制器
现在就可以任意拖动坐标轴了
另外还有这些
controls.enableDamping = true//设置阻尼惯性
controls.dampingFactor = 0.05//设置阻尼系数
controls.autoRotate = true//开启坐标轴自动旋转
提示:
const controls = new OrbitControls(camera,监听对象);
这里控制器的第二个参数是要监听的对象
const controls = new OrbitControls(camera,renderer.domElement);
// const controls = new OrbitControls(camera,document.body); //这里控制器的第二个参数是要监听的对象,我们也可以换成document.body或者别的都可以
// (不过留意一下,监听对象要换成document.body的话,记得要在css那给body加上宽高才能看得到)
2、位移
我们默认几何体的位置是在坐标轴的中心点(x,y,z)-(0,0,0)
那么用“网格.position.x”、“网格.position.y”、“网格.position.z”
或者“网格.position.set(x值,y值,z值)”
这两种方法都可以让物体位置偏移
//位置偏移
// cube.position.x = 2
cube.position.set(2,0,0)
那么注意,跟平面的前端布局一样,当没有父盒子元素时平面的元素里最大的就是body,所有外边距、内边距啥的都是以body为参照;这里也一样,没有父元素时,最大就是场景,元素位置以世界坐标轴为参照;但是当有了父盒子之后,子盒子的参照物就成了父盒子
这里讲一下怎么添加父盒子
//创建几何体
const geometry = new THREE.BoxGeometry(1,1,1);
//设置材质
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const father_material = new THREE.MeshBasicMaterial({ color: 0xff9800 })//这里为了区分父子,给父亲也提供一个新的材质
// 创建网格
const cube = new THREE.Mesh(geometry, material);
const fatherCube = new THREE.Mesh(geometry, father_material);//创建父元素,还是由原来的几何体组成,只不过变了新材质
//位置偏移
cube.position.x = 2
fatherCube.position.x = 0 //这里初始化父元素在x轴0的位置
//将网格添加到场景
// scene.add(cube)
scene.add(fatherCube) //现在在场景里添加的是父元素
fatherCube.add(cube) //在父元素里添加子元素
3、缩放大小
用“网格.scale(x值,y值,z值)”可以放大缩小物体
//放大缩小
cube.scale.set(2,2,2)
跟位移一样,它也是相对的,如果把父元素变大或缩小,那么子元素也会参照父元素的大小与之改变
cube.scale.set(2,2,2)
fatherCube.scale.set(0.5,0.5,0.5)
4、旋转
用“网格.ratation.x”、“网格.ratation.y”、“网格.ratation.z”就可以旋转了,在前面的demo里我们也利用这个进行了自动旋转
cube.rotation.x = Math.PI / 4 // Π/4 就是旋转 45°
同样,相对父元素的
cube.rotation.x = Math.PI / 4 // Π/4 就是旋转 45°
fatherCube.rotation.x = - (Math.PI / 4)
5、响应式画布以及全屏控制
当我们要缩放浏览器窗口、或者不同的浏览器有不同的尺寸时,我们就需要及时调整场景以及相机的大小宽高比
物品们只需要设置一个对window的监听器,当window窗口出发了“resize”更改大小的事件时,及时的更新场景以及相机大小
//监听窗口已完成响应式画布
window.addEventListener('resize', ()=>{
//更新变化场景大小
scene.setSize(window.innerWidth , innerHeight);
//更新相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
//更新相机投影矩阵
camera.updateProjectionMatrix();
})
然后我们还可以设置一个按钮来实现全屏的效果
//全屏控制
//新建一个按钮
var btn = document.createElement("button")
//设置一下按钮的样式
btn.innerHTML = '点击全屏'
btn.style.width = '180px'
btn.style.height = '80px'
btn.style.position = 'absolute'
btn.style.top = '10px'
btn.style.left = '10px'
btn.style.zIndex = '999'
btn.onclick = function(){
//请求全屏
document.body.requestFullscreen();
}
//最后追加到body中
document.body.appendChild(btn);
还有退出全屏(其实摁Esc就够了,有也行没有也行)
//这是退出全屏
var btn2 = document.createElement("button")
//设置一下按钮的样式
btn2.innerHTML = '退出全屏'
btn2.style.width = '180px'
btn2.style.height = '80px'
btn2.style.position = 'absolute'
btn2.style.top = '10px'
btn2.style.left = '300px'
btn2.style.zIndex = '999'
btn2.onclick = function(){
//退出全屏(当然其实摁Esc就够了)
document.exitFullscreen()
}
//最后追加到body中
document.body.appendChild(btn2);
当然你如果觉得太麻烦懒得记的话,也可以先复制保存,以后要用到的时候直接复制粘贴就好了,反正就是一个响应式画布以及全屏而已嘛
四、lil-GUI便捷可视化操作
前面我们学的内容里,位移、旋转、全屏按钮......都贼几把麻烦,要写一堆代码,那么用了lil-GUI就可以实现可视化操作
1、先导入
//导入lil.gui
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
2、然后创建一个对象,里面包含我们想要执行的一些函数方法
比如我们把前面全屏的操作删掉,用一个叫eventObj的对象装起来
let eventObj = {
FullScreean(){
document.body.requestFullscreen()
},
exitFullScreen(){
document.exitFullscreen()
}
}
3、然后创建GUI对象
然后调用GUI对象的add方法,把对象传入第一个参数,要执行的方法传到第二个参数(注意是字符串)
const gui = new GUI()
gui.add(eventObj , "FullScreean")
gui.add(eventObj , "exitFullScreen")
这里就多了可视化按钮
还可以换成中文的按钮
gui.add(eventObj , "FullScreean").name('全屏')
gui.add(eventObj , "exitFullScreen").name('退出全屏')
4、改变样式的方法
还可以操控位移、缩放、旋转等等,不过注意,因为【网格.位移】、【网格.缩放】、【网格.旋转】这些都是对象,所以就不用单独创建一个对象来给gui添加,直接添加
第一种写法:直接在GUI.add()方法里传四个参数
第一个参数是【网格.位移】、【网格.缩放】、【网格.旋转】这些操作
第二个参数是“x”、“y”、“z”
第三个参数和第四个参数是可变化的范围界限
例子:
gui.add(cube.position, "x", -5, 5).name('绿色盒子x轴平移')
第二种写法:在GUI.add()方法里传两个参数,然后.最小值().最大值().变化频数()......
例子:
gui.add(cube.position, "x").min(-5).max(5).step(1).name('绿色盒子x轴平移')
gui.add(cube.position, "y").min(-5).max(5).step(1).name('绿色盒子y轴平移')
gui.add(cube.position, "z").min(-5).max(5).step(1).name('绿色盒子z轴平移')
然后有的时候我们需要把缩放、平移......这些操作都包装一下,就可以调用gui.addFolder()来创建一个文件夹,然后用【文件夹.add()】的方式来操作
//第一个文件夹分类
let folder1 = gui.addFolder('绿色立方体的变化')
folder1.add(cube.position, "x").min(-5).max(5).step(1).name('绿色盒子x轴平移')
folder1.add(cube.position, "y").min(-5).max(5).step(1).name('绿色盒子y轴平移')
folder1.add(cube.position, "z").min(-5).max(5).step(1).name('绿色盒子z轴平移')
folder1.add(cube.rotation, "x").min(0).max(2*Math.PI).step(0.01).name('绿色盒子x轴旋转')
folder1.add(cube.rotation, "y").min(0).max(2*Math.PI).step(0.01).name('绿色盒子y轴旋转')
folder1.add(cube.rotation, "z").min(0).max(2*Math.PI).step(0.01).name('绿色盒子z轴旋转')
folder1.add(cube.scale, "x").min(-5).max(5).step(1).name('绿色盒子x轴缩放')
folder1.add(cube.scale, "y").min(-5).max(5).step(1).name('绿色盒子y轴缩放')
folder1.add(cube.scale, "z").min(-5).max(5).step(1).name('绿色盒子z轴缩放')
//第二个文件夹分类
let folder2 = gui.addFolder('橙色立方体的变化')
folder2.add(fatherCube.position, "x").min(-5).max(5).step(1).name('橙色盒子x轴平移')
folder2.add(fatherCube.position, "y").min(-5).max(5).step(1).name('橙色盒子y轴平移')
folder2.add(fatherCube.position, "z").min(-5).max(5).step(1).name('橙色盒子z轴平移')
folder2.add(fatherCube.rotation, "x").min(0).max(2*Math.PI).step(0.01).name('橙色盒子x轴旋转')
folder2.add(fatherCube.rotation, "y").min(0).max(2*Math.PI).step(0.01).name('橙色盒子y轴旋转')
folder2.add(fatherCube.rotation, "z").min(0).max(2*Math.PI).step(0.01).name('橙色盒子z轴旋转')
folder2.add(fatherCube.scale, "x").min(-5).max(5).step(1).name('橙色盒子x轴缩放')
folder2.add(fatherCube.scale, "y").min(-5).max(5).step(1).name('橙色盒子y轴缩放')
folder2.add(fatherCube.scale, "z").min(-5).max(5).step(1).name('橙色盒子z轴缩放')
folder2.add(father_material, "wireframe").name('橙盒子材质为线框模式')
5、操作完之后返回数据
那我们做这些可不是为了好玩就没了啊,我们通过可视化操作调好了位置之后,得知道这个位置在哪啊,那就直接调用它们的:onChange、onFinishChange两个触发事件
顾名思义,onChange就只要变化了就实时触发返回信息,而onFinishChange只有当最后停止了操作,才会触发返回值,看例子
onChange
folder1
.add(cube.position, "x")
.min(-5)
.max(5)
.step(1)
.name('绿色盒子x轴平移')
.onChange( value => {
console.log(`现在绿盒子沿X轴平移到了:${value}`)
})
onFinishChange
folder1
.add(cube.scale, "z")
.min(-5)
.max(5)
.step(1)
.name('绿色盒子z轴缩放')
.onFinishChange( value => {
console.log(`最终确定绿盒子向y轴缩放到:${value}`)
})
6、调整材质
我们还可以通过add方法传入material材质对象、以及"wireframe"属性来控制是否显示线框材质,(“wireframe”是布尔值,true就是开启)
folder1.add(material, "wireframe").name('绿盒子材质为线框模式')
folder2.add(father_material, "wireframe").name('橙盒子材质为线框模式')
7、调整颜色
调颜色有一丢丢麻烦,首先要定义装有颜色变量的一个对象,然后通过【addColor】(注意不是单单只是add了),传入对象参数以及颜色变量属性,就可以更改颜色了
而要想该完的颜色能显示出来,我们还得调整【材质】,【网格.材质.color.set()】才可以调整成功颜色(这里注意,直接用材质的固定名material,而不是相对于个别盒子设置的自定义材质的名字!!!)
比如我前面设置了父盒子的材质是father_material就不行,只能是material
let colorParams = {
cubeColor: '0x00ff00',
fatherCubeColor: '0xff9800'
}
folder1
.addColor(colorParams, 'cubeColor')
.name('子盒子的颜色调整')
.onChange( value => {
cube.material.color.set(value)
})
folder2
.addColor(colorParams, 'fatherCubeColor')
.name('父盒子的颜色调整')
.onChange( value => {
fatherCube.material.color.set(value)
})