示例
tsx
import * as THREE from 'three'
// import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import Stats from 'stats.js'
import { useConfigStore } from '@/stores/configStore'
const configStore = useConfigStore()
const win = window as any
// fpc 显示
// var stats = new Stats()
// stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom
// document.body.appendChild(stats.dom)
let scene, camera, renderer, mouse, raycaster
let models: any[] = [] // 用于存储加载的模型
let isMouseOver
let threeWidth = 800
let threeHeight = 500
let defaultCameraPosition = { x: -2.5, y: 2.9, z: 14 }
const keyboardGroup = new THREE.Group()
const loader = new GLTFLoader()
/********************************************************************************
* @brief: three 初始化
* @return {*}
********************************************************************************/
export const threeInit = (parentBox: HTMLElement, size: object) => {
threeWidth = size.width
threeHeight = size.height
// 场景和摄像机
scene = new THREE.Scene()
camera = new THREE.PerspectiveCamera(45, threeWidth / threeHeight, 0.1, 1000)
camera.position.set(
defaultCameraPosition.x,
defaultCameraPosition.y,
defaultCameraPosition.z
)
camera.lookAt(0, 0, 0)
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(threeWidth, threeHeight)
renderer.setClearColor(0xff0000, 0) // 透明背景
document.body.appendChild(renderer.domElement)
// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.8)
directionalLight.position.set(1, 1, 1).normalize()
scene.add(directionalLight)
// 轴辅助线
// const axesHelper = new THREE.AxesHelper(5)
// scene.add(axesHelper)
// 鼠标和射线
raycaster = new THREE.Raycaster()
mouse = new THREE.Vector2()
// 加载键盘模型
loadPadModel()
// 监听鼠标移动
renderer.domElement.addEventListener('mousemove', onMouseMove, false)
renderer.domElement.addEventListener('mouseout', onMouseOut, false)
renderer.domElement.addEventListener('mousedown', onMouseDown, false)
keyboardGroup.rotation.x = 1.2
keyboardGroup.position.x = 0.6
keyboardGroup.position.y = 0.45
parentBox.appendChild(renderer.domElement)
animate()
}
/********************************************************************************
* @brief: 加载键盘模型
* @return {*}
********************************************************************************/
const loadPadModel = async () => {
// 加载外壳
await loadModel(
'./padThree/back.gltf',
{ x: 0, y: 0, z: 0 },
{
color: 0xdfe1e1,
},
true,
true
)
// 加载大屏幕
await loadModel(
'./padThree/bScreen.gltf',
{ x: 0, y: 0, z: 0 },
{
color: 0x5d5f64,
}
)
// 加载小屏幕
await loadModel(
'./padThree/sScreen.gltf',
{ x: 0, y: 0, z: 0 },
{
color: 0x5d5f64,
}
)
// 加载按键
for (let i = 0; i < 8; i++) {
await loadModel(
`./padThree/key${i}.gltf`,
{ x: 0, y: 0, z: 0 },
{
color: 0x3c3e3e,
}
)
}
// 加载旋钮
for (let i = 0; i < 3; i++) {
await loadModel(
`./padThree/encoder${i}.gltf`,
{ x: 0, y: 0, z: 0 },
{
color: 0x3c3e3e,
}
)
}
}
/********************************************************************************
* @brief: 模型加载函数
* @param {*} url
* @param {*} position
* @param {*} material
* @return {*}
********************************************************************************/
function loadModel(url, position, material, isOpacity = false, isLine = false) {
return new Promise((resolve, reject) => {
loader.load(
url,
(gltf) => {
const model = gltf.scene
model.position.set(position.x, position.y, position.z)
// 遍历模型子对象,设置材质
model.traverse((child) => {
if (child.isMesh) {
child.material = new THREE.MeshStandardMaterial(material)
}
})
// 设置透明材质
if (isOpacity) {
model.traverse((child) => {
if (child.isMesh) {
child.material.transparent = true
child.material.opacity = 0.4 // 调整透明度
}
})
}
if (isLine) {
model.traverse((child) => {
if (child.isMesh) {
const edges = new THREE.EdgesGeometry(child.geometry)
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x8e8d8e,
})
const outline = new THREE.LineSegments(edges, lineMaterial)
child.add(outline) // 将轮廓线添加到模型子对象上
}
})
}
// 添加模型到组
keyboardGroup.add(model)
scene.add(keyboardGroup)
models.push(model)
resolve(true)
},
undefined,
(error) => {
console.error('An error happened while loading the model', error)
reject(error)
}
)
})
}
/********************************************************************************
* @brief: 鼠标移动事件
* @param {*} event
* @return {*}
********************************************************************************/
function onMouseMove(event) {
let box = renderer.domElement.getBoundingClientRect()
// 归一化鼠标位置
mouse.x = ((event.clientX - box.x) / box.width) * 2 - 1
mouse.y = -((event.clientY - box.y) / box.height) * 2 + 1
// 根据鼠标位置和相机生成射线
raycaster.setFromCamera(mouse, camera)
// 检测射线和场景中对象的交集
const intersects = raycaster.intersectObjects(models, true)
// 重置所有模型的颜色
models.forEach((model) => {
model.traverse((child) => {
if (child.isMesh) {
if (models.findIndex((o) => child.parent.uuid == o.uuid) > 2) {
// gsap.to(child.position, {
// y: 1,
// duration: 2,
// ease: 'power2.out',
// })
child.material.color.set(0x3c3e3e)
}
}
})
})
// 改变鼠标悬停模型的颜色
if (intersects.length > 0) {
// 获取最接近的对象
const hoveredObject = intersects[0].object
hoveredObject.traverse((child) => {
// console.info(child)
if (child.isMesh) {
if (models.findIndex((o) => child.parent.uuid == o.uuid) > 2) {
child.material.color.set(0xe5e4e4) // 设置悬停颜色
}
}
})
isMouseOver = true
// 动态调整摄像机视角
camera.position.x = defaultCameraPosition.x + mouse.x * 2
camera.position.y = defaultCameraPosition.y + mouse.y * 2
// camera.lookAt(keyboardGroup.position)
camera.lookAt(0, 0, 0)
} else {
isMouseOver = false
}
}
/********************************************************************************
* @brief: 鼠标移出事件处理
* @return {*}
********************************************************************************/
function onMouseOut() {
isMouseOver = false
// 恢复摄像机到固定视角
camera.position.set(
defaultCameraPosition.x,
defaultCameraPosition.y,
defaultCameraPosition.z
)
// camera.lookAt(keyboardGroup.position)
camera.lookAt(0, 0, 0)
}
/********************************************************************************
* @brief: 鼠标按下事件
* @param {*} event
* @return {*}
********************************************************************************/
function onMouseDown(event) {
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects(models, true)
if (intersects.length > 0) {
let selectedModel = intersects[0].object // 设置被点击的模型
selectedModel.traverse((child) => {
if (child.isMesh) {
let index = models.findIndex((o) => child.parent.uuid == o.uuid)
if (index > 2) {
// console.info(index)
selectedModel.material.color.set(0xecd6d1)
// 切换编辑的单键,发送信息到主窗口
let tempObj: object = {}
tempObj['configIndex'] = index - 3
win.myApi.setConfigStore(tempObj)
// 设置当前窗口的单键编辑索引
configStore.setConfigIndex(index - 3)
// 获取设置索引的配置数据
win.myApi.setConfigStore({
get: 'keyConfig',
})
}
}
})
}
}
/********************************************************************************
* @brief: 调整窗口大小
* @return {*}
********************************************************************************/
export const resizeThreeBox = (size: object) => {
threeWidth = size.width
threeHeight = size.height
camera.aspect = threeWidth / threeHeight
camera.updateProjectionMatrix()
renderer.setSize(threeWidth, threeHeight)
}
/********************************************************************************
* @brief: 动画开始
* @return {*}
********************************************************************************/
function animate() {
requestAnimationFrame(animate)
// stats.begin()
// stats.end()
renderer.render(scene, camera)
}模型导入并自动旋转
- 自动旋转模型:在
animate函数中,通过不断更新模型的rotation属性实现自动旋转。 - 居中模型:可以使用
model.position设置位置,或者使用 Three.js 的Box3计算模型的边界并自动居中。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Three.js Centered and Rotating Model</title>
<style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
<script>
let scene, camera, renderer, model, mixer;
const clock = new THREE.Clock();
function init() {
// 场景和摄像机
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1, 5);
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0); // 透明背景
document.body.appendChild(renderer.domElement);
// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(1, 1, 1).normalize();
scene.add(directionalLight);
// 加载模型
const loader = new THREE.GLTFLoader();
loader.load('path/to/your/model.gltf', (gltf) => {
model = gltf.scene;
// 居中模型
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center); // 将模型位置设置为居中
// 动画初始化
mixer = new THREE.AnimationMixer(model);
if (gltf.animations.length > 0) {
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
}
scene.add(model);
}, undefined, (error) => {
console.error('An error happened while loading the model', error);
});
// 窗口大小调整
window.addEventListener("resize", onWindowResize);
animate();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
// 更新动画
if (mixer) mixer.update(clock.getDelta());
// 自动旋转模型
if (model) {
model.rotation.y += 0.01; // 设置旋转速度
}
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>代码详解
- 模型自动旋转:在
animate函数中,通过不断更新model.rotation.y实现水平自动旋转。可以通过调整旋转增量0.01修改旋转速度。 - 模型居中:使用
Box3获取模型的边界,再通过model.position.sub(center)将模型移动到场景中心。 - 窗口调整:在
resize事件中调整相机和渲染器的比例,以保持居中和旋转效果。 - 动画更新:如果模型包含动画,
mixer.update(clock.getDelta())会更新动画时间,确保动画顺利播放。