three.js是一款基于原生web GL封装通用Web 3D引擎,在小游戏、产品展示、物联网、数字孪生、智慧城市园区、机械、建筑、全景看房、GIS等各个领域基本上都有three.js的身影。
一、three.js学习指南官网
Three.js中文网提供Three.js、WebGL视频课程http://www.webgl3d.cn/
二、引入three.js
npm install three@0.157.0 -S
三、素材资源准备
1)准备模型,在public下新建model文件夹,把模型放到model文件夹下
2)复制three.js 依赖包自带的draco文件夹复制到public下
draco的位置如下:node_modules->three->examples->jsm->libs->draco
将该文件夹复制到public下
3)准备hdr纹理贴图,如果不需要纹理贴图的可以忽略
四、代码实现
<template>
<div>
<!-- 进度条 -->
<div class="pro-box">
<el-progress v-if="showProcess" :stroke-width="22" :percentage="loadProcess" />
</div>
</div>
</template>
<script setup>
import {
ref,
getCurrentInstance,
watch
} from "vue";
import {
tag,
labelRenderer
} from "@/label.js";
import * as THREE from "three";
import {
OrbitControls
} from "three/examples/jsm/controls/OrbitControls";
import {
GLTFLoader
} from "three/examples/jsm/loaders/GLTFLoader"; // 加载gltf模型
import {
DRACOLoader
} from "three/examples/jsm/loaders/DRACOLoader"; // 解压gltf模型
import {
GUI
} from "three/examples/jsm/libs/lil-gui.module.min.js";
import {
RGBELoader
} from "three/examples/jsm/loaders/RGBELoader"; // 加载hdr环境光照
// import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer"; // 后期处理
// import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass"; // 后期处理
// import gsap from "gsap";
// ******************
const {
proxy
} = getCurrentInstance();
const loadProcess = ref(0);
const showProcess = ref(true);
const labelBox = ref(tag());
const modelObj = ref(null);
// 进度条
watch(loadProcess, (value) => {
if (value == 100) {
setTimeout(() => {
showProcess.value = false;
}, 2000);
}
});
// ******************
// 初始化场景
// const model = ref(null);
const scene = new THREE.Scene();
// 初始化相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(8, 5, 4);
camera.lookAt(0, 0, 0); // 相机观察目标
camera.updateProjectionMatrix(); //更新投影矩阵
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
// 设置抗锯齿
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 设置色调映射
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.shadowMap.enabled = true; //允许阴影
// AxesHelper: 辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(10);
scene.add(axesHelper);
// 初始化相机轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 加载环境纹理
let rebgLoader = new RGBELoader();
rebgLoader.load("./textues/sky2.hdr", function (texture) {
// 设置球形纹理映射
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
scene.environment = texture;
});
// 初始化加载器draco
const dracoLoader = new DRACOLoader();
// 设置draco路径
dracoLoader.setDecoderPath("./draco/");
const gltfLoader = new GLTFLoader();
// 设置gltf加载器draco解码器
gltfLoader.setDRACOLoader(dracoLoader);
// 加载模型
gltfLoader.load("./model/jc.glb", function (gltf) {
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh) {
child.material.side = THREE.DoubleSide; // 模型双面渲染
child.castShadow = true; // 光照是否有阴影
child.receiveShadow = true; // 是否接收阴影
child.frustumCulled = false;
}
});
model.position.set(0, -3, 0);
scene.add(model);
// gui
const gui = new GUI();
gui.add(model.position, "x").name("模型坐标轴x位置");
gui.add(model.position, "y").name("模型坐标轴y位置");
gui.add(model.position, "z").name("模型坐标轴z位置");
}, function (xhr) {
// 计算加载进度
const percent = xhr.loaded / xhr.total * 100;
loadProcess.value = parseInt(percent);
// console.log('加载进度------', parseInt(loadProcess.value),parseInt(percent));
});
// 点光源
const pointLight = new THREE.PointLight(0xffffff, 10); //光源颜色 光照强度
pointLight.decay = 0.0; //设置光源不随距离衰减 默认2.0
pointLight.position.set(5, 10, 0); // 点光源位置
pointLight.castShadow = true;
scene.add(pointLight);
// 环境光设置
const ambient = new THREE.AmbientLight(0xffffff, 1);
scene.add(ambient);
// 添加平行光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 60, 10);
light.target.position.set(0, 4, -50);
scene.add(light);
// 平行光辅助观察
// const lightHelper = new THREE.DirectionalLightHelper(light, 1000);
// scene.add(lightHelper);
// lightHelper.position.set(300, 200, 120);
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
}
render();
document.body.addEventListener("click", function (event) {
const px = event.offsetX;
const py = event.offsetY;
const Sx = event.clientX; //鼠标单击位置横坐标
const Sy = event.clientY; //鼠标单击位置纵坐标
//屏幕坐标转WebGL标准设备坐标
// 这里的window.innerWidth和window.innerHeight其实是容器的宽高,并且默认按照(0,0)来计算的,需要考虑偏移的位置(px,py)
const x = (Sx / renderer.domElement.clientWidth) * 2 - 1;
const y = -(Sy / renderer.domElement.clientHeight) * 2 + 1;
//创建一个射线投射器Raycaster
const raycaster = new THREE.Raycaster();
//通过鼠标单击位置标准设备坐标和相机参数计算射线投射器`Raycaster`的射线属性.ray
raycaster.setFromCamera(new THREE.Vector2(x, y), camera); //参数: 标准化的二维坐标 相机
//返回.intersectObjects()参数中射线选中的网格模型对象
// 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
const intersects = raycaster.intersectObjects(scene.children); //参数 计算范围
console.log(intersects, '----------intersects')
if (intersects.length > 0) {
modelObj.value = intersects[0].object;
// console.log(intersects[0], '--------------intersects[0]')
labelBox.value.position.set(intersects[0].point.x, intersects[0].point.y, intersects[0].point.z);
// labelBox.value.element.innerHTML = modelObj.value.name + '哈哈哈哈哈';
labelBox.value.element.innerHTML = `
<div>
<div>
<div>名称:${modelObj.value.name}</div>
<div>提示:哈哈哈哈哈</div>
</div>
</div>
`;
labelBox.value.element.style.visibility = 'visible';
} else {
labelBox.value.element.style.visibility = 'hidden'; //没有选中mesh,隐藏标签
}
scene.add(labelBox.value);
labelRenderer.render(scene, camera); // 标签渲染
});
</script>
<style scoped>
* {
margin: 0;
padding: 0;
}
canvas {
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
}
.pro-box {
position: absolute;
left: 50px;
top: 30px;
width: 500px;
height: 10px;
}
</style>
label.js
import {
CSS2DRenderer,
CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer.js";
import labelBg from "@/assets/tips-border.png"; // 导入标签背景
// 创建一个HTML标签
function tag() {
// 创建div元素(作为标签)
var div = document.createElement("div");
div.style.visibility = "hidden";
div.innerHTML = "";
div.style.width = "200px";
div.style.height = "100px";
div.style.padding = "5px 10px";
div.style.color = "#fff";
div.style.fontSize = "16px";
div.style.position = "absolute";
// div.style.backgroundColor = "rgba(25,25,25,0.5)";
div.style.background = `rgba(25,25,25,0.5) url(${labelBg})no-repeat center center`;
div.style.backgroundSize = "100% 100%";
div.style.borderRadius = "5px";
//div元素包装为CSS2模型对象CSS2DObject
var label = new CSS2DObject(div);
div.style.pointerEvents = "none"; //避免HTML标签遮挡三维场景的鼠标事件
// 设置HTML元素标签在three.js世界坐标中位置
// label.position.set(x, y, z);
return label; //返回CSS2模型标签
}
// 创建一个CSS2渲染器CSS2DRenderer
var labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = "absolute";
// // 避免renderer.domElement影响HTMl标签定位,设置top为0px
labelRenderer.domElement.style.top = "0px";
labelRenderer.domElement.style.left = "0px";
// //设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型
labelRenderer.domElement.style.pointerEvents = "none";
document.body.appendChild(labelRenderer.domElement);
export { tag, labelRenderer };
五、效果展示
这世界很喧嚣,做你自己就好