前言 :
three.js 基础模型加载可查看 https://threejs.org/ 上面涵盖各种模型的加载器及加载方式,在此不再赘述。
本文主要介绍如何使用vue配合three.js 库实现各种格式3d 模型文件的加载,组合式模型文件的加载,以及如何去更新相机的设置,调整模型的大小缩放去达到最适合观察的角度与大小。
一,模型加载器引入
three.js 官网之中提供了各种模型文件所对应的加载器,在此处只是简单列举常用模型加载器,其余加载器的使用方式于此大致无二。
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js';
import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';
import { AMFLoader } from 'three/examples/jsm/loaders/AMFLoader.js';
import { GCodeLoader } from 'three/examples/jsm/loaders/GCodeLoader.js';
import { KMZLoader } from 'three/examples/jsm/loaders/KMZLoader.js';
import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader.js';
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
this.modeLoader = {
'gltf': () => { return new GLTFLoader() },
'gcode': () => { return new GCodeLoader() },
'glb': () => { return new GLTFLoader() },
'fbx': () => { return new FBXLoader() },
'3ds': () => { return new TDSLoader() },
'3mf': () => { return new ThreeMFLoader() },
'amf': () => { return new AMFLoader() },
'kmz': () => { return new KMZLoader() },
'obj': () => { return new OBJLoader() },
'pcd': () => { return new PCDLoader() },
'ply': () => { return new PLYLoader() },
'stl': () => { return new STLLoader() },
'dae': () => { return new ColladaLoader() },
'mtl': () => { return new MTLLoader() },
}
以上便是常用加载器的列举及其初始化,在此处将各种模型放在一个对象中为了便于后面可以更加方便去通过对象属性的方式拿到此加载器的实例。
二,场景初始化
在此,准备工作便已经全部完成,接下来便开始场景,相机,渲染器,控制器等必不可少的元素的初始化及其属性配置。
init() {
this.camera = new Three.PerspectiveCamera(45, this.container.clientWidth / this.container.clientHeight, 0.1, 1000)
this.camera.position.set(0, 0, 100)
this.camera.updateProjectionMatrix();
this.scene = new Three.Scene()
const color = new Three.Color(0x800080);
color.convertSRGBToLinear();
this.renderer = new Three.WebGLRenderer({
antialias: true,
})
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
this.renderer.gammaFactor = 2.2;
this.renderer.outputEncoding = Three.sRGBEncoding
this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比
this.container.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
this.animate()
}
舞台已经打好了,接下来该有我们的主人公出场了,接下来便是在场景中加载模型文件。
三,单个模型加载
loadModel() {
const { type, url } = this.config
const loader = this.modeLoader[type]()
if(['gltf','glb'].includes(type)){
const dracoloader = this.getDracoLoader()
loader.setDRACOLoader(dracoloader)
}
loader.load(url, (object) => {
let obj = object
if (['stl', 'ply'].includes(type)) {
let material = new Three.MeshPhongMaterial({
color: 0x4eacc2,
specular: 0x111111,
shininess: 200
})
obj = new Three.Mesh(object, material);
} else if (['glb', 'gltf', 'kmz', 'dae'].includes(type)) {
obj = object.scene
}
this.scene.add(obj)
this.initSize(obj)
})
}
其中根据模型的类型做了处理,对与压缩模型进行了单独处理
const dracoloader = this.getDracoLoader()
loader.setDRACOLoader(dracoloader)
解决压缩模型
const getDracoLoader = () => {
const dracoloader = new DRACOLoader()
dracoloader.setDecoderPath("/draco/")
dracoloader.setDecoderConfig({ type: "js" })
dracoloader.preload()
return dracoloader
}
到此,模型就已经出现在场景中了,但是在此还是有很多问题,比如说,模型的大小,有些很大,有些很小甚至在场景中只能看见一个小点只能通过鼠标滚轮去放大,才能让他展示在我们面前,那有没有好的办法让他直接已最好的姿态展示在我们面前呢,当然可以,接下来我们要请出灯光师去给我们的舞台加上灯光,让摄影师去调整相机的角度去让模型更好展示。
灯光加载
环境光(AmbientLight):光没有特定方向,只是整体改变场景的光照明暗。
平行光(DirectionalLight):光就是沿着特定方向发射。这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果。
setLight() {
const light1 = new Three.DirectionalLight(0xffffff, 1)
light1.position.set(0, 0, 1)
this.scene.add(light1)
const light2 = new Three.DirectionalLight(0xffffff, 1)
light2.position.set(0, 2, 100)
this.scene.add(light2)
const light3 = new Three.DirectionalLight(0xffffff, 1)
light3.position.set(0, 0, -10)
this.scene.add(light3)
const ambientLight = new Three.AmbientLight(0xffffff, 1)
this.scene.add(ambientLight)
}
模型大小设置:
initSize(obj) {
let group = obj;
group.updateMatrixWorld()
const box = new Three.Box3().setFromObject(group);
const size = box.getSize(new Three.Vector3());
const center = box.getCenter(new Three.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
const targetSize = 2.5; // 目标大小
const scale = targetSize / (maxSize > 1 ? maxSize : .5);
group.scale.set(scale, scale, scale)
this.controls.maxDistance = size.length() * 10
this.camera.lookAt(center)
this.camera.updateProjectionMatrix();
}
到此,模型就以完美的姿态展现在您的面前了。。。
四,组合模型加载(以obj和mtl 为例)
loadGroupModel() {
const { type, url } = this.config
const typeList = type.split(',')
let t1 = typeList[0]
let t2 = typeList[1]
const loader1 = modeLoader[t1]()
const loader2 = modeLoader[t2]()
loader1.load(url[0], (materials) => {
materials.preload()
loader2.setMaterials(materials)
loader2.load(url[1], (object) => {
scene.add(object)
initSize(object)
})
})
}
完整代码 :
import * as Three from 'three'
import { onMounted, reactive, defineComponent, h } from 'vue';
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js';
import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';
import { AMFLoader } from 'three/examples/jsm/loaders/AMFLoader.js';
import { GCodeLoader } from 'three/examples/jsm/loaders/GCodeLoader.js';
import { KMZLoader } from 'three/examples/jsm/loaders/KMZLoader.js';
import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader.js';
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
class LoadModel {
constructor(config, elementId) {
this.config = config
this.container = document.getElementById(elementId)
// 相机
this.camera
// 场景
this.scene
//渲染器
this.renderer
// 控制器
this.controls
// 模型
this.model
this.modeLoader = {
'gltf': () => { return new GLTFLoader() },
'gcode': () => { return new GCodeLoader() },
'glb': () => { return new GLTFLoader() },
'fbx': () => { return new FBXLoader() },
'3ds': () => { return new TDSLoader() },
'3mf': () => { return new ThreeMFLoader() },
'amf': () => { return new AMFLoader() },
'kmz': () => { return new KMZLoader() },
'obj': () => { return new OBJLoader() },
'pcd': () => { return new PCDLoader() },
'ply': () => { return new PLYLoader() },
'stl': () => { return new STLLoader() },
'dae': () => { return new ColladaLoader() },
'mtl': () => { return new MTLLoader() },
}
this.init()
this.setLight()
this.loadModel()
}
animate() {
requestAnimationFrame(() => this.animate())
this.renderer.render(this.scene, this.camera)
this.controls.update()
}
init() {
this.camera = new Three.PerspectiveCamera(45, this.container.clientWidth / this.container.clientHeight, 0.1, 1000)
this.camera.position.set(0, 0, 100)
this.camera.updateProjectionMatrix();
this.scene = new Three.Scene()
const color = new Three.Color(0x800080);
color.convertSRGBToLinear();
this.renderer = new Three.WebGLRenderer({
antialias: true,
})
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
this.renderer.gammaFactor = 2.2;
this.renderer.outputEncoding = Three.sRGBEncoding
this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比
this.container.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
this.animate()
}
setLight() {
const light1 = new Three.DirectionalLight(0xffffff, 1)
light1.position.set(0, 0, 1)
this.scene.add(light1)
const light2 = new Three.DirectionalLight(0xffffff, 1)
light2.position.set(0, 2, 100)
this.scene.add(light2)
const light3 = new Three.DirectionalLight(0xffffff, 1)
light3.position.set(0, 0, -10)
this.scene.add(light3)
const ambientLight = new Three.AmbientLight(0xffffff, 1)
this.scene.add(ambientLight)
}
// 设置模型大小
initSize(obj) {
let group = obj;
group.updateMatrixWorld()
const box = new Three.Box3().setFromObject(group);
const size = box.getSize(new Three.Vector3());
const center = box.getCenter(new Three.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
const targetSize = 2.5; // 目标大小
const scale = targetSize / (maxSize > 1 ? maxSize : .5);
group.scale.set(scale, scale, scale)
this.controls.maxDistance = size.length() * 10
this.camera.lookAt(center)
this.camera.updateProjectionMatrix();
}
getDracoLoader() {
const dracoloader = new DRACOLoader()
dracoloader.setDecoderPath("/draco/")
dracoloader.setDecoderConfig({ type: "js" })
dracoloader.preload()
return dracoloader
}
loadModel() {
const { type, url } = this.config
const loader = this.modeLoader[type]()
if(['gltf','glb'].includes(type)){
const dracoloader = this.getDracoLoader()
loader.setDRACOLoader(dracoloader)
}
loader.load(url, (object) => {
let obj = object
if (['stl', 'ply'].includes(type)) {
let material = new Three.MeshPhongMaterial({
color: 0x4eacc2,
specular: 0x111111,
shininess: 200
})
obj = new Three.Mesh(object, material);
} else if (['glb', 'gltf', 'kmz', 'dae'].includes(type)) {
obj = object.scene
}
this.scene.add(obj)
this.initSize(obj)
})
}
loadGroupModel() {
const { type, url } = this.config
const typeList = type.split(',')
let t1 = typeList[0]
let t2 = typeList[1]
const loader1 = modeLoader[t1]()
const loader2 = modeLoader[t2]()
loader1.load(url[0], (materials) => {
materials.preload()
loader2.setMaterials(materials)
loader2.load(url[1], (object) => {
scene.add(object)
initSize(object)
})
})
}
}
export default LoadModel
结语 :
既然选择了远方,便只顾风雨兼程。