介绍
是用threejs调用模型, 主要功能有兼容pc和移动端的监听鼠标移动和触摸屏幕 呈现的3D 旋转效果, 和放大放小功能, 自转功能, 和调用模型动画效果
# 🎏 使用场景
这里使用的是本地的js资源库, 可以自行下载,或者使用远程的cdn
<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/jsm/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
全代码, 通过url 传入对应的 模型路径, 自定义模型
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>3D模型交互</title>
<style>
@media screen and (min-width: 1000px) {
body {
margin: 0;
/* margin-top: 50px; */
overflow: hidden;
touch-action: none;
font-family: Arial, sans-serif;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
.controls {
position: fixed;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 20px;
z-index: 100;
}
button {
padding: 8px 16px;
border: none;
border-radius: 20px;
background: #4caf50;
color: white;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
button:hover {
background: #45a049;
}
button:active {
transform: scale(0.95);
}
.debug {
position: fixed;
top: 60px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
}
}
@media screen and (max-width: 1000px) {
body {
margin: 0;
/* margin-top: 50px; */
overflow: hidden;
touch-action: none;
font-family: Arial, sans-serif;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
.controls {
position: fixed;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 20px;
z-index: 100;
display: flex;
justify-content: space-between;
width: 70%;
}
button {
padding: 8px 16px;
border: none;
border-radius: 20px;
background: #4caf50;
color: white;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
font-size: 12px;
}
button:hover {
background: #45a049;
}
button:active {
transform: scale(0.95);
}
.debug {
position: fixed;
top: 60px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
}
}
.clone {
position: absolute;
top: 60px;
right: 20px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 24px;
border-radius: 50%;
border: solid 1px #fff;
padding: 3px;
}
</style>
</head>
<body>
<div
class="clone"
id="clone">
×
</div>
<canvas id="webglCanvas"></canvas>
<div class="controls">
<button id="playBtn">播放</button>
<button id="zoomIn">放大</button>
<button id="zoomOut">缩小</button>
<button id="rotateBtn">自转: 关</button>
</div>
<div
class="debug"
id="debugInfo">
初始化中...
</div>
<!-- 引入Three.js库 -->
<script src="./three.min.js"></script>
<script src="./GLTFLoader.js"></script>
<script src="./OrbitControls.js"></script>
<!-- 调用uniapp的方法库 -->
<script src="./uni.webview.1.5.1.js"></script>
<script>
// 动态获取URL参数,自定义模型链接
function getUrlParameters() {
var params = {},
url = window.location.href,
query = url.split('?')[1];
if (query) {
var queryArr = query.split('&');
for (var i = 0; i < queryArr.length; i++) {
var pair = queryArr[i].split('=');
params[pair[0]] = decodeURIComponent(pair[1]);
}
}
return params;
}
var params = getUrlParameters();
console.log(params);
let gitfUrl = params.url;
// app中调用返回上一页的方法
document.getElementById('clone').addEventListener('click', () => {
uni.navigateBack();
// uni.postMessage({
// data: {
// action: 'click'
// }
// });
});
// 调试信息输出
function debugLog(message) {
document.getElementById('debugInfo').textContent = message;
console.log(message);
}
// 初始化场景 背景颜色
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
// 初始化相机高度, 视角,纵横比, 近裁剪面,远裁剪面
// 60度视角, 纵横比为窗口宽高比, 近裁剪面0.1,远裁剪面1000
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
// 设置相机位置,x,y,z坐标
// x轴:左右,y轴:上下,z轴:前后,z轴正方向为前方
camera.position.set(0, 1, 5);
// 初始化渲染器
// 1. 这三行代码的执行顺序很重要:
// a. 先创建渲染器实例
// b. 然后设置像素比
// c. 最后设置尺寸
//
// 2. 如果不设置pixelRatio,在高DPI设备上可能出现模糊
//
// 3. setSize()实际上做了两件事:
// a. 调整canvas元素的CSS样式尺寸
// b. 设置WebGL渲染缓冲区的实际分辨率
//
// 4. 内存占用:
// 最终缓冲区大小 = (width * pixelRatio) * (height * pixelRatio) * 4(bytes/pixel)
// 例如1920x1080屏幕DPI=2 → 实际缓冲区约16MB
const renderer = new THREE.WebGLRenderer({
// 指定渲染器绑定的canvas DOM元素
// 这里使用getElementById获取id为'webglCanvas'的HTML canvas元素
canvas: document.getElementById('webglCanvas'),
// 开启抗锯齿(Anti-aliasing)
// 抗锯齿可以平滑渲染对象的边缘,减少锯齿状像素边缘
// 会轻微影响性能,但能显著提升视觉质量
antialias: true
});
// 设置设备像素比(Device Pixel Ratio)
// 这个操作可以适配高DPI屏幕(如Retina显示屏)
// window.devicePixelRatio获取物理像素与CSS像素的比率
// 例如Retina屏的值为2,普通屏幕为1
// 设置后可以避免在高DPI设备上出现模糊渲染
renderer.setPixelRatio(window.devicePixelRatio);
// 设置渲染器的输出尺寸
// 参数为宽度和高度(单位:像素)
// 这里设置为整个浏览器窗口的尺寸
// window.innerWidth/innerHeight获取视口的CSS像素尺寸
// 注意:实际渲染缓冲区大小会乘以devicePixelRatio
// 例如:窗口宽1000px,DPI=2 → 实际渲染分辨率2000px
renderer.setSize(window.innerWidth, window.innerHeight);
// 添加光照
// 创建环境光(Ambient Light)实例
// 参数1: 光的颜色(十六进制RGB),0xffffff表示白色光
// 参数2: 光的强度,1.2表示120%强度(范围通常0-1,但可以超过)
const ambientLight = new THREE.AmbientLight(0xffffff, 1.2);
// 将环境光添加到场景中
// 环境光会均匀照亮场景中的所有物体
// 特点:没有方向、不产生阴影、全局均匀照明
scene.add(ambientLight);
// 创建平行光(Directional Light)实例
// 参数1: 光的颜色,0xffffff为纯白色
// 参数2: 光的强度,0.8表示80%强度
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
// 设置平行光的位置
// 参数依次为X,Y,Z坐标
// 平行光的方向是从设置的位置指向坐标原点(0,0,0)
// 这里设置为(2,3,4)表示光线从右上前方照射
directionalLight.position.set(2, 3, 4);
// 将平行光添加到场景中
// 平行光特点:
// - 有明确方向
// - 可以产生阴影
// - 模拟太阳光等远距离光源
scene.add(directionalLight);
// 添加坐标轴辅助(调试用)
// const axesHelper = new THREE.AxesHelper(5);
// scene.add(axesHelper);
// 模型和动画变量
// 模型对象变量
// 类型: THREE.Group | THREE.Object3D
// model.traverse() - 遍历模型所有子对象
// model.position.set() - 设置模型位置
let model,
// 动画混合器(AnimationMixer)实例
// 类型: THREE.AnimationMixer
// 作用: 控制模型动画的播放、暂停和混合
// 核心功能:
// 1. 管理动画时间轴
// 2. 处理多个动画的混合
// 3. 派发动画事件
// 注意: 每个需要动画的模型需要独立的mixer
mixer,
// 动画动作(AnimationAction)实例
// 类型: THREE.AnimationAction
// 作用: 控制单个动画剪辑的播放状态
// 关键属性:
// .time - 当前动画时间(秒)
// .timeScale - 播放速度(1=正常,-1=倒放)
// .loop - 循环模式
// 注意: 一个mixer可以创建多个action控制不同动画
action;
// 动画播放状态标志
let isPlaying = false;
// 动画方向标志
// 类型: Boolean
// 作用: 标记当前动画播放方向
// 取值说明:
// false - 正向播放(0->结束)
// true - 反向播放(结束->0)
let isReversed = false;
// 自动旋转标志
// 类型: Boolean
// 作用: 控制模型是否自动旋转
// 典型实现:
// 在渲染循环中检测:
// if(autoRotate) model.rotation.y += 0.01
let autoRotate = false;
// 加载模型 使用GLTFLoader加载模型文件
const loader = new THREE.GLTFLoader();
debugLog('开始加载模型...');
loader.load(
gitfUrl,
// 参数2: 加载成功回调函数
(gltf) => {
// 获取模型场景对象
model = gltf.scene;
// 设置模型初始缩放(1,1,1表示原始大小)
// 注意:某些模型可能需要调整这个值避免过大/过小
model.scale.set(1, 1, 1);
// 设置模型Y轴位置(将模型抬高0.5单位)
// 这个调整通常用于让模型"站在"地面上
model.position.y = 0.5;
// 初始化动画 // 检查模型是否包含动画数据
if (gltf.animations && gltf.animations.length > 0) {
// 如果有动画,创建动画混合器(Animation Mixer)
// Animation Mixer用于处理模型的动画
// 参数:需要绑定动画的模型对象
mixer = new THREE.AnimationMixer(model);
// 获取第一个动画剪辑(AnimationClip)并创建动画动作
action = mixer.clipAction(gltf.animations[0]);
// 设置动画循环模式为单次循环(LoopOnce)
// 设置动画循环模式:
// THREE.LoopOnce - 只播放一次
// 第二个参数1表示重复次数(这里设为1次)
action.setLoop(THREE.LoopOnce, 1);
// 动画播放完后保持在最后一帧
// 如果设为false,动画会跳回第一帧
action.clampWhenFinished = true;
// 初始状态设为暂停
// 这样可以在点击播放按钮时开始动画
action.paused = true;
// 监听动画完成事件
mixer.addEventListener('finished', () => {
// 更新播放状态
isPlaying = false;
// 切换播放方向标记
isReversed = !isReversed;
// 更新按钮显示
updatePlayButton();
});
debugLog(`模型加载完成,找到动画: ${gltf.animations[0].name}`);
} else {
// debugLog('模型加载完成,未找到动画');
}
// 将模型添加到场景中
scene.add(model);
// 更新播放按钮状态
updatePlayButton();
},
// 参数3: 加载进度回调
(xhr) => {
// 计算加载进度百分比(保留1位小数)
const percent = ((xhr.loaded / xhr.total) * 100).toFixed(1);
debugLog(`模型加载中: ${percent}%`);
},
(error) => {
debugLog(`模型加载失败: ${error.message}`);
console.error(error);
}
);
// 交互控制
// 标记当前是否正在拖拽
let isDragging = false;
// 存储上一次的指针位置
let previousPosition = { x: 0, y: 0 };
// 鼠标/触摸事件 指针按下事件处理(兼容鼠标/触摸)
function onPointerDown(event) {
// 设置拖拽状态为true
isDragging = true;
// 保存当前指针位置:
// event.clientX/clientY - 鼠标事件坐标
// event.touches[0].clientX/clientY - 触摸事件坐标
previousPosition = {
x: event.clientX || event.touches[0].clientX,
y: event.clientY || event.touches[0].clientY
};
// 1. 阻止默认行为防止页面滚动
// 2. 改变鼠标指针样式
}
// 指针移动事件处理
function onPointerMove(event) {
// 检查是否处于拖拽状态且模型已加载
if (!isDragging || !model) return;
// 获取当前指针位置(兼容处理)
const clientX = event.clientX || event.touches[0].clientX;
const clientY = event.clientY || event.touches[0].clientY;
// 计算指针移动增量(当前帧与上一帧的差值)
const deltaX = clientX - previousPosition.x;
const deltaY = clientY - previousPosition.y;
// 根据水平移动旋转模型(Y轴旋转)
// 0.01是旋转灵敏度系数,值越大旋转越快
model.rotation.y += deltaX * 0.01;
// 根据垂直移动旋转模型(X轴旋转)
// 使用THREE.MathUtils.clamp限制旋转范围// 限制不要垂直翻转
// 参数:值, 最小值(-90度), 最大值(90度)
model.rotation.x = THREE.MathUtils.clamp(model.rotation.x + deltaY * 0.01, -Math.PI / 2, Math.PI / 2);
// 更新记录的位置
previousPosition = { x: clientX, y: clientY };
}
// 指针释放事件处理
function onPointerUp() {
// 重置拖拽状态
isDragging = false;
}
// 事件监听(兼容鼠标和触摸) // 获取渲染器的canvas元素
const canvas = renderer.domElement;
// 鼠标按下
canvas.addEventListener('mousedown', onPointerDown);
// 鼠标移动
canvas.addEventListener('mousemove', onPointerMove);
// 鼠标释放
canvas.addEventListener('mouseup', onPointerUp);
// 鼠标离开画布
canvas.addEventListener('mouseleave', onPointerUp);
// 触摸事件监听(移动端适配)
// { passive: false } 允许阻止默认滚动行为
canvas.addEventListener('touchstart', onPointerDown, { passive: false });
canvas.addEventListener('touchmove', onPointerMove, { passive: false });
canvas.addEventListener('touchend', onPointerUp);
// 控制按钮功能 // 更新播放按钮状态函数
function updatePlayButton() {
// 获取播放按钮DOM元素
const btn = document.getElementById('playBtn');
if (!action) {
btn.textContent = '无动画';
btn.disabled = true; // 禁用按钮交互
return;
}
// 根据播放状态更新按钮文本
if (isPlaying) {
btn.textContent = '暂停';
} else {
btn.textContent = isReversed ? '倒放' : '播放';
}
// 启用按钮
btn.disabled = false;
}
// 播放按钮点击事件
document.getElementById('playBtn').addEventListener('click', () => {
if (!action) return;
if (isPlaying) {
// 暂停逻辑
action.paused = true; // 暂停动画播放
isPlaying = false; // 更新状态标志
} else {
// 设置播放方向和起始点
action.timeScale = isReversed ? -1 : 1;
action.time = isReversed ? action.getClip().duration : 0; // 设置起始点
// 重置并播放动画(reset()清除之前的混合效果)
action.reset().play();
// 更新状态标志
isPlaying = true;
}
// 立即更新按钮状态
updatePlayButton();
});
// 放大按钮事件
document.getElementById('zoomIn').addEventListener('click', () => {
// 安全校验 + 放大1.1倍
if (model) model.scale.multiplyScalar(1.1);
});
// 缩小按钮事件
document.getElementById('zoomOut').addEventListener('click', () => {
// 安全校验 + 缩小到0.9倍
if (model) model.scale.multiplyScalar(0.9);
});
// 自转按钮事件
document.getElementById('rotateBtn').addEventListener('click', () => {
autoRotate = !autoRotate; // 切换自转状态
document.getElementById('rotateBtn').textContent = `自转: ${autoRotate ? '开' : '关'}`;
});
// 窗口大小调整 // 窗口大小响应式处理
window.addEventListener('resize', () => {
// 更新相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 必须调用此方法更新相机投影矩阵
camera.updateProjectionMatrix();
// 调整渲染器尺寸(同步canvas的CSS和绘图缓冲区尺寸)
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 动画循环 // 创建Three.js时钟对象(用于精确时间计算)
const clock = new THREE.Clock();
function animate() {
// 请求下一帧动画(形成递归循环)
requestAnimationFrame(animate);
// 更新动画
if (mixer) {
// 计算时间增量(秒) // 更新动画系统(需传入时间差)
mixer.update(clock.getDelta());
}
// 自转逻辑
if (autoRotate && model) {
model.rotation.y += 0.005; // 每帧旋转0.005弧度
}
// 执行渲染
renderer.render(scene, camera);
}
// 启动动画循环
animate();
debugLog('渲染循环已启动');
</script>
</body>
</html>
# 多模型切换版本
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>3D模型交互演示</title>
<style>
body {
margin: 0;
overflow: hidden;
touch-action: none;
font-family: Arial, sans-serif;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
.controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-wrap: nowrap;
justify-content: center;
gap: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 20px;
z-index: 100;
max-width: 90%;
}
button,
select {
padding: 8px 16px;
border: none;
border-radius: 20px;
background: #4caf50;
color: white;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
}
button:hover {
background: #45a049;
}
button:active {
transform: scale(0.95);
}
select {
background: #2196f3;
min-width: 120px;
}
.debug {
position: fixed;
top: 60px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
}
.clone {
position: absolute;
top: 60px;
right: 20px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 24px;
border-radius: 50%;
border: solid 1px #fff;
padding: 3px;
}
</style>
</head>
<body>
<div
class="clone"
id="clone">
×
</div>
<canvas id="webglCanvas"></canvas>
<div class="controls">
<button id="modelSelector">切换</button>
<button id="playBtn">播放</button>
<button id="zoomIn">放大</button>
<button id="zoomOut">缩小</button>
<button id="rotateBtn">自转: 关</button>
</div>
<div
class="debug"
id="debugInfo">
初始化中...
</div>
<!-- 引入Three.js库 -->
<script src="./three.min.js"></script>
<script src="./GLTFLoader.js"></script>
<!-- 调用uniapp的方法库 -->
<script src="./uni.webview.1.5.1.js"></script>
<script>
// 动态获取URL参数,自定义模型链接
function getUrlParameters() {
var params = {},
url = window.location.href,
query = url.split('?')[1];
if (query) {
var queryArr = query.split('&');
for (var i = 0; i < queryArr.length; i++) {
var pair = queryArr[i].split('=');
params[pair[0]] = decodeURIComponent(pair[1]);
}
}
return params;
}
var params = getUrlParameters();
console.log(params);
let gitfUrl = params.url;
// app中调用返回上一页的方法
document.getElementById('clone').addEventListener('click', () => {
uni.navigateBack();
// uni.postMessage({
// data: {
// action: 'click'
// }
// });
});
// 调试信息输出
function debugLog(message) {
document.getElementById('debugInfo').textContent = message;
console.log(message);
}
// 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
// 初始化相机
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1, 5);
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('webglCanvas'),
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// 添加光照
const ambientLight = new THREE.AmbientLight(0xffffff, 1.2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(2, 3, 4);
scene.add(directionalLight);
// 模型和动画变量
let model, mixer, action;
let isPlaying = false;
let isReversed = false;
let autoRotate = false;
let isDragging = false;
let lastPos = { x: 0, y: 0 };
let modeIndex = 0;
const modelConfig = [
{
// name:'模型',
url: './demo.glb',
scale: 1,
position: { y: 1 }
},
{
url: './gui/source/Tortoise.gltf',
scale: 1,
position: { y: 1 }
},
{
url: './scene.gltf',
scale: 1,
position: { y: 1 }
}
];
if (gitfUrl) {
// 如果有那么就重新写入数组
let urlList = gitfUrl.split(',');
modelConfig.length = 0; // 清空原有数组
urlList.forEach((item, index) => {
modelConfig.push({
url: item,
scale: 1,
position: { y: 1 }
});
});
} else {
// 如果没有那么就默认数组的模型
console.log('没有找到urlList');
}
// window.addEventListener('message', function (event) {
// console.log(event.data); // 输出: {data: 'Hello from uni-app'}
// alert(event.data); // 弹出: Hello from uni-app
// const value = localStorage.getItem('urlList'); //从浏览器获取 url 缓存
// console.log(value); // 输出: value
// if (value) {
// // 如果有那么就重新写入数组
// let urlList = value.split(',');
// modelConfig.length = 0; // 清空原有数组
// urlList.forEach((item, index) => {
// modelConfig.push({
// url: item,
// scale: 1,
// position: { y: 1 }
// });
// });
// } else {
// // 如果没有那么就默认数组的模型
// console.log('没有找到urlList');
// }
// });
// 模型配置
// const modelConfig = {
// flamingo: {
// url: './demo.glb',
// scale: 1,
// position: { y: 1 }
// },
// parrot: {
// url: './gui/source/Tortoise.gltf',
// scale: 1,
// position: { y: 1 }
// },
// stork: {
// url: './scene.gltf',
// scale: 1,
// position: { y: 1 }
// }
// };
// 模型加载器
const loader = new THREE.GLTFLoader();
// 加载并切换模型
async function loadModel(index) {
debugLog(`正在加载模型: ${index}`);
try {
const gltf = await new Promise((resolve, reject) => {
loader.load(
modelConfig[index].url,
resolve,
(xhr) => {
const percent = ((xhr.loaded / xhr.total) * 100).toFixed(1);
debugLog(`加载进度: ${percent}%`);
},
reject
);
});
// 移除旧模型
if (model) {
scene.remove(model);
if (mixer) {
mixer.stopAllAction();
mixer.uncacheRoot(model);
}
}
// 设置新模型
model = gltf.scene;
model.scale.setScalar(modelConfig[index].scale);
model.position.y = modelConfig[index].position.y;
model.rotation.y = Math.PI / 2; // 初始朝向调整
// 初始化动画
if (gltf.animations && gltf.animations.length > 0) {
mixer = new THREE.AnimationMixer(model);
action = mixer.clipAction(gltf.animations[0]);
action.setLoop(THREE.LoopOnce, 1);
action.clampWhenFinished = true;
action.paused = true;
mixer.addEventListener('finished', () => {
isPlaying = false;
isReversed = !isReversed;
updatePlayButton();
});
debugLog(`找到动画: ${gltf.animations[0].name}`);
} else {
debugLog('未找到动画数据');
}
scene.add(model);
updatePlayButton();
debugLog(`模型加载完成: 模型${index + 1}`);
} catch (error) {
debugLog(`模型加载失败: ${error.message}`);
console.error(error);
throw error;
}
}
// 更新播放按钮状态
function updatePlayButton() {
const btn = document.getElementById('playBtn');
if (!action) {
btn.textContent = '无动画';
btn.disabled = true;
return;
}
if (isPlaying) {
btn.textContent = '暂停';
} else {
btn.textContent = isReversed ? '倒放' : '播放';
}
btn.disabled = false;
}
// 交互控制
function onPointerDown(e) {
isDragging = true;
lastPos = {
x: e.clientX || e.touches[0].clientX,
y: e.clientY || e.touches[0].clientY
};
}
function onPointerMove(e) {
if (!isDragging || !model) return;
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const deltaX = clientX - lastPos.x;
const deltaY = clientY - lastPos.y;
model.rotation.y += deltaX * 0.01;
model.rotation.x = THREE.MathUtils.clamp(model.rotation.x + deltaY * 0.01, -Math.PI / 2, Math.PI / 2);
lastPos = { x: clientX, y: clientY };
}
function onPointerUp() {
isDragging = false;
}
// 滚轮缩放功能
function onMouseWheel(e) {
if (!model) return;
// 阻止页面滚动
e.preventDefault();
// 计算缩放方向(缩小或放大)
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
// 应用缩放
model.scale.multiplyScalar(zoomFactor);
// 限制缩放范围(可选)
model.scale.clampScalar(0.1, 5);
}
// 事件监听
const canvas = renderer.domElement;
canvas.addEventListener('mousedown', onPointerDown);
canvas.addEventListener('mousemove', onPointerMove);
canvas.addEventListener('mouseup', onPointerUp);
canvas.addEventListener('mouseleave', onPointerUp);
// 添加滚轮事件监听
canvas.addEventListener('wheel', onMouseWheel, { passive: false });
canvas.addEventListener('touchstart', onPointerDown, { passive: false });
canvas.addEventListener('touchmove', onPointerMove, { passive: false });
canvas.addEventListener('touchend', onPointerUp);
// 控制按钮功能
document.getElementById('playBtn').addEventListener('click', () => {
if (!action) return;
if (isPlaying) {
action.paused = true;
isPlaying = false;
} else {
action.timeScale = isReversed ? -1 : 1;
action.time = isReversed ? action.getClip().duration : 0;
action.reset().play();
isPlaying = true;
}
updatePlayButton();
});
document.getElementById('zoomIn').addEventListener('click', () => {
if (model) model.scale.multiplyScalar(1.1);
});
document.getElementById('zoomOut').addEventListener('click', () => {
if (model) model.scale.multiplyScalar(0.9);
});
document.getElementById('rotateBtn').addEventListener('click', () => {
autoRotate = !autoRotate;
document.getElementById('rotateBtn').textContent = `自转: ${autoRotate ? '开' : '关'}`;
});
// 模型选择器
// document.getElementById('modelSelector').addEventListener('change', (e) => {
// loadModel(e.target.value).catch(console.error);
// });
document.getElementById('modelSelector').addEventListener('click', () => {
if (modeIndex >= modelConfig.length - 1) {
modeIndex = 0;
} else {
modeIndex = modeIndex + 1;
}
loadModel(modeIndex).catch(console.error);
});
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 动画循环
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
// 更新动画
if (mixer) {
mixer.update(clock.getDelta());
}
// 自转逻辑
if (autoRotate && model && !isDragging) {
model.rotation.y += 0.005;
}
renderer.render(scene, camera);
}
// 初始加载
loadModel(0)
.then(() => {
animate();
debugLog('系统初始化完成');
})
.catch(console.error);
</script>
</body>
</html>
