Three.js - JS三维模型库在Vue2中的基础教程
官网 :https://threejs.org/案例: https://threejs.org/examples/#webgl_animation_keyframes
threejs 官网 :https://threejs.org/
threejs 案例: https://threejs.org/examples/#webgl_animation_keyframes
threejs API:https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene
开始使用
npm install --save three;
全局引用
import * as THREE from 'three';
按需引用 ,demo均使用全局
import {class} from 'three';
package-lock.json
"three": "^0.151.3",
"three-orbitcontrols": "^2.110.3",
"vue": "^2.6.14",
一、基础使用
三大核心
new THREE.WebGLRenderer()// 创建渲染器
new THREE.Scene() // 实例化场景
new THREE.PerspectiveCamera()// 实例化相机
创建四个文件方便场景管理
渲染控制器ThreeController.js 作为 3d渲染的主要入口文件
ThreeController.js
import * as THREE from 'three';
export const renderer = new THREE.WebGLRenderer() // 创建渲染器
export const scene = new THREE.Scene() // 实例化场景
export const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000) 相机
export class ThreeController {
Model = null;
scene = null;
constructor(Model) { //构造器函数
Model.appendChild(renderer.domElement) //容器
renderer.setSize(Model.offsetWidth, Model.offsetHeight, true)
this.Model = Model
this.scene = scene
camera.position.set(100, 100, 100) // 设置相机位置
camera.lookAt(new THREE.Vector3(0, 0, 0)) // 设置相机看先中心点
camera.up = new THREE.Vector3(0, 1, 0) // 设置相机自身的方向
renderer.shadowMap.enabled = true;
renderer.render(scene, camera);
}
// 外部访问将模型添加到场景中
addObject(...object) {
object.forEach(elem => {
this.scene.add(elem)
})
}
}
HomeView.vue
<template>
<div>
<div class="three-canvas" ref="threeTarget"></div>
</div>
</template>
import {ThreeController,} from "@/components/ThreeController";
import { ModelListConfig} from"@/components/ModelListConfig";
import { LightList } from "@/components/LightListConfig";
import { allHelper } from "@/components/AxesHelperConfig";
return { ThreeController: null,
}
mounted() {
this.init();
},
methods: {
init() {
this.ThreeController = newThreeController(this.$refs.threeTarget);
this.ThreeController.addObject(...ModelListConfig);
this.ThreeController.addObject(...LightList);
this.ThreeController.addObject(...allHelper);
},
此时场景中一片漆黑
接下来添加模型 ModelListConfig.js
import * as THREE from 'three';
具体模型类型参考Api
export const ModelListConfig = [] 存储模型数组,也可某个模型单独导出
const sky = new THREE.TextureLoader().load(Require('sky.jpg'))
创建材质贴图
export const MeshModel = new THREE.Mesh( 创建几何体
new THREE.BoxGeometry(20, 20, 20), 正方体 size 20
new THREE.MeshStandardMaterial({ 材质纹理
color: 'rgb(36, 172, 242)',
// roughness: 0 , 光滑度 0最光滑
// metalness: 0, 金属度 1最像金属
map: sky
})
)
ModelListConfig.push(MeshModel) 将模型添加到数组中
// 多人开发用来存储数据
MeshModel.userData = {
name: 'MeshModel',
user: '我是正方体模型'
}
可以看到场景依旧是漆黑,所以要“开灯”即添加光线
接下来添加光线 LightListConfig.js
import * as THREE from 'three';
export const LightList = []
// // 添加环境光(自然光),设置自然光的颜色,设置自然光的强度(0 最暗, 1 最强)
const hemiLight = new THREE.HemisphereLight("#A09E9E", 0.5);
hemiLight.position.set(0, 40, 15);
LightList.push(hemiLight)
const hemiLighthelper = new THREE.HemisphereLightHelper(hemiLight, 5);
LightList.push(hemiLighthelper)
export const pointLight = new THREE.PointLight(
'rgb(255,255,255)',
0.7,
600,
0.2
)
pointLight.position.set(0, 1, 50) // 设置点光源位置 (x,y,z)
LightList.push(pointLight)
这时模型就可以正常显示了
创建辅助线 AxesHelperConfig.js
import { AxesHelper ,GridHelper } from 'three'
/**
* 场景中添加辅助线
* @param {allHelper.push(new AxesHelper())}
* 添加栅格
* @param {allHelper.push(new GridHelper())}
*/
export const allHelper = []
// 坐标辅助
export const axesHelper = new AxesHelper(200) // 创建坐标辅助 (500 为辅助线的长度)
export const gridHelper = new GridHelper(500, 20, 'green', 'rgb(255, 255, 255)')
allHelper.push(gridHelper,axesHelper) // 添加到辅助列表
/*
gridHelper Config
size -- 坐标格尺寸. 默认为 10. 这就是网格组成的大正方形最大是多少
divisions -- 坐标格细分次数. 默认为 10. 组成的最大的正方向平均分多少份
colorCenterLine -- 中线颜色. 值可以为 Color 类型, 16进制 和 CSS 颜色名. 默认为 0x444444。这个是指网格和坐标的 x轴 z 轴重合线的颜色。
colorGrid -- 坐标格网格线颜色. 值可以为 Color 类型, 16进制 和 CSS 颜色名. 默认为 0x888888
*/
效果
二、射线控制器
相机视角拖拽 ThreeController.js 需要射线控制器 OrbitControls 因为渲染成是三维之后,我们点击的是相机呈现的二维浏览器画面,距离模型到页面上是有一定距离的,简单来说就像我们站在一个物体面前,用手机拍照时,点击的照片中的物体,实际上我们点击的并不是物体,而是相机渲染给我们的一个二维照片。OrbitControls,会穿透整个三维场并返回一个list,第[0]项就是我们想要点击模型。
ThreeController.js
import { OrbitControls }from'three/examples/jsm/controls/OrbitControls'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
const mouse = new THREE.Vector2() // 初始化鼠标位置
const raycaster = new THREE.Raycaster()//初始化射线发射器
// 屏幕鼠标x,屏幕鼠标y 视图宽度,视图高度
let x = 0; let y = 0; let width = 0; let height = 0
constructor(Model) {
…
EventInjection(camera) //要在渲染器之前
…
renderer.render(scene, camera);
}
export const EventInjection=()=>{
// 鼠标移动事件
const transformControls = new TransformControls(camera, renderer.domElement)
renderer.domElement.addEventListener("mousemove", event => {
x = event.offsetX
y = event.offsetY
width = renderer.domElement.offsetWidth
height = renderer.domElement.offsetHeight
mouse.x = x / width * 2 - 1
mouse.y = -y * 2 / height + 1
})
let transing = false
transformControls.addEventListener("mouseDown", event => {
transing = true
return event
})
// 鼠标点击事件
renderer.domElement.addEventListener("click", event => {
if (transing) {
transing = false
return
}
scene.remove(transformControls) // 移除变换控制器
transformControls.enabled = false // 停用变换控制器
raycaster.setFromCamera(mouse, camera) // 配置射线发射器,传递鼠标和相机对象
const intersection = raycaster.intersectObjects(scene.children) // 获取射线发射器捕获的模型列表,传进去场景中所以模型,穿透的会返回我们
if (intersection.length) {
const object = intersection[0].object // 获取第一个模型
console.log(object)
scene.add(transformControls) // 添加变换控制器
transformControls.enabled = true // 启用变换控制器
transformControls.attach(object)
}
return event
})
// 监听变换控制器模式更改
document.addEventListener("keyup", event => {
if (transformControls.enabled) { // 变换控制器为启用状态执行
if (event.key === 'e') { // 鼠标按下e键,模式改为缩放
transformControls.mode = 'scale'
return false
}
if (event.key === 'r') { // 鼠标按下r键,模式改为旋转
transformControls.mode = 'rotate'
return false
}
if (event.key === 't') { // 鼠标按下t键,模式改为平移
transformControls.mode = 'translate'
return false
}
}
return event
})
// three.js自带的方法
const orbitControls = new OrbitControls(camera, renderer.domElement)
// console.log(MOUSE)//查看MOUSE中的配置项
orbitControls.mouseButtons = { // 设置鼠标功能键(轨道控制器)
LEFT: null, // 左键无事件
MIDDLE: THREE.MOUSE.DOLLY, // 中键缩放
RIGHT: THREE.MOUSE.ROTATE// 右键旋转
}
scene.add(transformControls)
}
这时我们在log里可以看到之前添加的 userdata
同时也可以做拽查看
三、进阶使用
1、外部导入模型 obj、 gltf、 json、 glb等。
开源模型地址 https://github.com/mrdoob/three.js/blob/master
开源的模型
可以自己改成其他格式,使用别的引用方法尝试
ThreeController.js 示例演示,导入glb文件
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
export const LoadingGLTFMethod=(GltfModel)=> {
loader.load(`${process.env.BASE_URL}model/${GltfModel}`, (gltf) => {
gltf.scene.scale.set(15, 15, 15)
gltf.scene.position.set(0, 0, 0)
gltf.scene.userData={
name:"LoadingGLTFModel",
data:"123",
id:"1212121212",
title:"人物模型"
}
let axis = new THREE.Vector3(0, 1, 0);//向量axis
gltf.scene.rotateOnAxis(axis, Math.PI);
gltf.scene.traverse(function (object) {
if (object.isMesh) {
object.castShadow = true; //阴影
object.receiveShadow = true; //接受别人投的阴影
}
})
scene.Model = gltf.scene;
scene.add(gltf.scene) //公用访问时使用常量向场景中添加引入的模型
return gltf.scene
})
}
LoadingGLTFMethod("Soldier.glb");//看一看模型有没有出现
可以看到已经成功导入
创建地板 ModelListConfig.js
export const Ground = new THREE.Mesh(new THREE.PlaneGeometry(300, 300), new THREE.MeshPhongMaterial({
color: 0x888888, depthWrite: true,
}));
ModelListConfig.push(Ground)
当然你也可以自己做一个材质,来作为地板的纹理
const Require = (src) => {
return require(`../assets/${src}`)
}
const GroundTexture = new THREE.TextureLoader().load(Require('RC.jpg'))
export const Ground = new THREE.Mesh(new THREE.PlaneGeometry(300, 300), new THREE.MeshPhongMaterial({
color: 0x888888, depthWrite: true,
map: GroundTexture
}));
Ground.rotation.x = - Math.PI / 2;
Ground.receiveShadow = true;
ModelListConfig.push(Ground) // 添加到模型数组
贴图效果:
上述我们在导入文件时,设定了导入的物体是接受投影的,也添加过自然光,这时发现并没有影子。在方向光的作用下,物体会形成阴影投影效果,Three.js物体投影模拟计算主要设置三部分,一个是设置产生投影的模型对象,一个是设置接收投影效果的模型,最后一个是光源对象本身的设置,光源如何产生投影。
LightListConfig.js 添加平行光
const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
directionalLight.position.set(0, 35, 20);// 设置光源位置
directionalLight.castShadow = true; // 设置用于计算阴影的光源对象
// 设置计算阴影的区域,最好刚好紧密包围在对象周围
// 计算阴影的区域过大:模糊 过小:看不到或显示不完整
directionalLight.shadow.camera.near =0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -10;
// 设置mapSize属性可以使阴影更清晰,不那么模糊
// directionalLight.shadow.mapSize.set(1024,1024)
LightList.push(directionalLight)
这时候我们加上外部控制 HomeView.vue
<template>
<div>
<ul>
<li><button @click="LoadingMethod()" >LoadingMethod</button></li>
<li><button @click="logs()">logs</button></li>
<li><button @click="LoadingTrack()">LoadingTrack</button></li>
<li><button @click="Rorate()">Rorate</button></li>
<li><button@click="LoadingSceneMaterials()">LoadingSceneMaterials
</button></li>
<li><button @click="run()">Run</button></li>
</ul>
<div class="log-content ">
<h6>name:{{userData.name}}</h6>
<h6>data:{{userData.data}}</h6>
<h6>id:{{userData.id}}</h6>
<h6>title:{{userData.title}}</h6>
</div>
<div class="three-canvas" ref="threeTarget"></div>
</div>
</template>
import {
ThreeController,
LoadingGLTFMethod,
scene,
}
from "@/components/ThreeController"; //中央渲染控制
return {
…
userData: {},
test: {},
};
methods: {
…
LoadingMethod() {
LoadingGLTFMethod("Soldier.glb");
},
logs() {
this.test = this.ThreeController.scene.children.find((item) => {
if(item.userData.name){
return item.userData.name == "LoadingGLTFModel";
}
});
this.userData=this.test.userData
},
}
<style lang="scss" scoped>
.three-canvas {
width: 100vw;
height: 100vh;
}
ul{
position: absolute;
right: 10px;
top: 10px;
}
.log-content {
width: 200px;
height: 60px;
position: absolute;
margin: 10px;
background-color: skyblue;
}
调用 LoadingMethod() 模型加载后,我们再调用logs() 查看我们加上的usedata
2. requestAnimationFrame 请求动画帧
它是一个浏览器的宏任务, requestAnimationFrame的用法与settimeout很相似,只是不需要设置时间间隔而已。requestAnimationFrame使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。它返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数的执行,它会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,如果系统绘制率是 60Hz,那么回调函数就会16.7ms再 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是,requestAnimationFrame的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
ThreeController.js 使用requestAnimationFrame() 让模型“run”起来
export const clock = new THREE.Clock();
export let mixer = null
export class ThreeController {
constructor(Model) {
… animate() //执行做好的动画帧
}
}
export const animate = () => { requestAnimationFrame(animate);
if (mixer) { mixer.update(clock.getDelta());}
renderer.render(scene, camera);
}
export const startAnimation=(skinnedMesh, animations, animationName)=>{
const m_mixer = new THREE.AnimationMixer(skinnedMesh);
const clip = THREE.AnimationClip.findByName(animations,animationName);
if (clip) {
const action = m_mixer.clipAction(clip);
action.play();
}
return m_mixer;
}
export const LoadingGLTFMethod=(GltfModel,type)=> {//这里新加了一个type
…
mixer = startAnimation(
gltf.scene,
gltf.animations,
gltf.animations[type].name // animationName,1 是"Run"
);
}
LoadingGLTFMethod("Soldier.glb",1);
效果 (不会做动图:))
此时模型仅仅是原地run ,这时我们加上手动控制
模型中有position属性,代表物体在空间中x、y、z轴的坐标
HomeView.vue
import {
…
renderer,
camera,
} from "@/components/ThreeController";
methods: {
…
Rorate(){
this.test = this.ThreeController.scene.children.find((item) => {
return item.userData.name == "LoadingGLTFModel";
});
this.animates();
},
animates() {
requestAnimationFrame(this.animates);
//this.test.position.x += 0.01;
//this.test.position.y += 0.01;
this.test.position.z += 0.1;
renderer.render(scene, camera);
},
}加载完模型后,执行Rorate(),这时它就是真正的往前run了
3、轨道以及轨迹思想
Line类是一种线形状几何体,物体运动的轨迹我们可以看成一条线,让模型围轨道运动
ModelListConfig.js
const curveArr = [0, 0, 0, 350, 0, 0, 0, 0, 350];
const curve = [];
for (let i = 0; i < curveArr.length; i += 3) { //每三个点生成一个坐标
curve.push(new THREE.Vector3(curveArr[i], curveArr[i + 1], curveArr[i + 2]));
}
const RoutePoints = new THREE.CatmullRomCurve3(curve, true)
const sphereCurve = RoutePoints.clone()
export const pathPoints = sphereCurve.getPoints(200)//取200个点
const line = new THREE.Line(
new THREE.BufferGeometry().setFromPoints(pathPoints),
new THREE.LineBasicMaterial({
color: "red",
linewidth: 1,
})
)
ModelListConfig.push(line)
为了更直观一些,我们还可以把取到的点也渲染上
const addMesh = () => {
let list = []
for (let point of pathPoints) {
const sphere = new THREE.BoxGeometry(3, 1, 1)
const sphereMaterial = new THREE.MeshBasicMaterial({ map: RouteTexture })
const sphereMesh = new THREE.Mesh(sphere, sphereMaterial)
sphereMesh.position.set(point.x, point.y, point.z)
ModelListConfig.push(sphereMesh)
list.push(sphereMesh)
}
list.push(line)
return list
}
// 加载轨道
export const LoadingTrack = () => {
return addMesh()
}
让物体围绕着生成的轨道运动 HomeView.vue
Return{ … num:0}
LoadingTrack(){ //加载出轨道
this.ThreeController.addObject(...LoadingTrack())
},
run(){
this.test = this.ThreeController.scene.children.find((item) => {
return item.userData.name == "LoadingGLTFModel";
});
this.runanimates();
},
runanimates(){
if(this.num<=pathPoints.length-2){
this.num+=1
}else{
this.num=0
}
requestAnimationFrame(this.runanimates);
this.test.position.x = pathPoints[ this.num].x ;
this.test.position.y = pathPoints[ this.num].y;
this.test.position.z = pathPoints[ this.num].z
renderer.render(scene, camera);
},
也可以用来实现相机漫游,实时光影 只不过就是运动的物体从模型,变成相机或者光线
加载场景材质,ThreeController.js 天空盒效果
export const SceneMapMaterial=(list)=>{
const map=new THREE.CubeTextureLoader()
.setPath( `${process.env.BASE_URL}model/` )
.load(list );
return map
}
HomeView.vue
SceneList:[ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg','nz.jpg' ]
LoadingSceneMaterials(){
scene.background = SceneMapMaterial(this.SceneList)//max px 1024*1024
},
名字有对应的作用,且不能超过最大限制
更多推荐
所有评论(0)