前言
前一篇文章我们介绍了three.js中的基础概念,并给出了展示整体流程的一个简单示例, 本文我们继续研究。
问题
我们在很多3d效果图上都能看到鼠标移动或者缩进实现旋转或者放大缩小的效果,这个在three.js中是通过OrbitControls
这个组件实现的。
早上在使用threejs引入OrbitControls的时候发现新版本(大约r159以后)的引入方式都是通过import来引入的,而我想在纯html中做简单测试,
然而如果在html中直接使用import又会提示出各种各样的错误,经过反复的踩坑和实验以及百度,终于解决了这个模块化导入的问题。
所以本文姑且算是一篇踩坑记录吧。
期望目标
本文我们还是通过简单示例来展示要描述的问题和解决方案。
最终实现的效果是两个虚线圆环,自动转动,也可以根据鼠标拖拽或者缩放来触发旋转和缩放效果。
关于虚线圆环的实现方式和参数设置不是本文重点,可参考本文后面的代码进行实现和测试。
开始
前面提到要实现鼠标控制旋转和缩放需要用到OrbitControls
这个组件,这个组件在three.js的源码中可以找到。
下载three.js源码
下载地址: https://gitee.com/mirrors/three.js
可以在标签
中选择版本进行下载,截止发文时的最新版本是r162
新建目录
目录结构如下图:
可以直接把下载下来的源码文件中的对应文件夹直接搬过来,也可以只在对应目录放置需要的文件(本文中主要是用到了three.js或者three.module.js/OrbitControls.js这两个文件)。
引入js
这里需要说明一下,在r159版本以前是提供纯js版本的OrbitControls的代码的,位置在examples/js/controls文件夹中,但是后面新出的版本(r160+)删除了js目录,新增了jsm目录,存放的是以模块化的方式实现的组件代码,这个模块化代码直接在html中引用的话控制台会报错。
这里直接说解决方案:
-
使用旧版本的OrbitControls.js文件,可以直接在html中引入。
<script src="static/r159/build/three.min.js"></script> <script src="static/r159/examples/js/OrbitControls.js"></script> <script> .... // 创建并初始化OrbitControls const controls = new THREE.OrbitControls(camera, renderer.domElement); </script>
-
使用新版本的模块化方式引入,需要调整引入方式
<script type="importmap"> { "imports": { "three": "../static/latest/build/three.module.js", "three/addons/": "../static/latest/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; .... // 创建并初始化OrbitControls const controls = new OrbitControls(camera, renderer.domElement); .... </script>
PS: 注意以上两种方案在实例化OrbitControls对象时是有区别的。
OrbitControls的介绍和属性配置
THREE.OrbitControls是Three.js库中一个非常实用的相机控制器,它允许用户通过鼠标或触摸设备以直观的方式旋转、缩放和平移3D场景
。这个控件主要是为了让用户能够交互式地查看3D模型或场景,模拟类似3D建模软件中的轨道摄像机操作。
以下是关键参数配置和示例:
用途 | 代码示例 |
---|---|
新建对象 | const controls = new OrbitControls(camera, renderer.domElement); |
设置相机围绕的目标点(焦点) | controls.target.set(x, y, z); // 默认为(0, 0, 0) |
是否启用控件 | controls.enabled = true //默认为true |
控制功能开关 | controls.enableRotate = true; // 是否允许旋转,默认为true controls.enableZoom = true; // 是否允许缩放,默认为true controls.enablePan = true; // 是否允许平移,默认为true |
限制范围 | controls.minDistance = 1; // 相机离目标的最小距离,默认值通常取决于场景大小 controls.maxDistance = 1000; // 相机离目标的最大距离 controls.minZoom = 0.1; // 最小缩放比例(如果支持) controls.maxZoom = 100; // 最大缩放比例(如果支持) |
旋转速度与灵敏度 | controls.rotateSpeed = 1.0; // 旋转速度,默认值为1 controls.zoomSpeed = 1.2; // 缩放速度,默认值为1 controls.panSpeed = 0.8; // 平移速度,默认值为0.8 |
旋转限制 | controls.minAzimuthAngle = -Math.PI / 2; // 最小左旋角度 controls.maxAzimuthAngle = Math.PI / 2; // 最大右旋角度 controls.minPolarAngle = 0; // 最小抬头角度(防止相机倒置) controls.maxPolarAngle = Math.PI / 2; // 最大低头角度(比如只允许鸟瞰视角) |
OrbitControls本质上就是改变相机的参数,比如相机的位置属性,改变相机位置也可以改变相机拍照场景中模型的角度,实现模型的360度旋转预览效果,改变透视投影相机距离模型的距离,就可以改变相机能看到的视野范围。
controls.addEventListener('change', function () {
// 浏览器控制台查看相机位置变化
console.log('camera.position',camera.position);
});
补充知识
1. importMap的使用场景
import map
是一种Web平台的原生模块加载映射机制,它允许开发者在浏览器中自定义模块导入路径与实际加载地址之间的映射关系。通过 <script type="importmap">
标签在HTML文档中定义一个 import map,可以解决以下使用场景:
- 模块路径重定向:
- 当项目依赖了多个库,而这些库可能因为版本更新、CDN地址变化或者内部模块结构调整等原因需要修改其导入路径时,import map 可以集中管理这些映射关系,无需更改代码中的
import
语句。
- 当项目依赖了多个库,而这些库可能因为版本更新、CDN地址变化或者内部模块结构调整等原因需要修改其导入路径时,import map 可以集中管理这些映射关系,无需更改代码中的
- 命名空间或包结构支持:
- 在Node.js环境和一些构建工具中,开发者习惯于使用类似 npm 的包管理和导入方式(如
import { someModule } from 'package-name'
)。import map 提供了一种在浏览器环境中模拟这种行为的方法,使得大型项目能够更好地组织和维护模块间的依赖关系。
- 在Node.js环境和一些构建工具中,开发者习惯于使用类似 npm 的包管理和导入方式(如
- 多版本共存与按需加载:
- 同一项目中可能需要同时使用不同版本的库,import map 可以将不同的模块版本映射到不同的URL上,实现多个版本的同时加载与使用,避免版本冲突。
- 优化加载策略:
- 开发者可以通过 import map 将模块的源码映射到经过编译、压缩或缓存优化后的URL上,从而提升加载速度和用户体验。
- 本地开发与生产环境切换:
- 在开发阶段,可能需要从本地文件系统加载模块;而在部署上线后,则需要从CDN或其他远程服务器加载。import map 可以灵活配置这些差异化的加载路径。
以下为示例代码
<script type="importmap">
{
"imports": {
"module-a": "/path/to/module-a.js",
"module-b": "//cdn.example.com/module-b.js",
"package-name": "/local/path/to/package-name/index.js"
}
}
</script>
然后在JavaScript模块中就可以按照映射关系来导入模块:
import { someFunction } from 'module-a';
import * as packageApi from 'package-name';
2. type=“module”的使用场景
type="module"
属性在HTML <script>
标签中使用,用于指示浏览器按照ECMAScript模块(ES6 Modules)的规范来加载和执行JavaScript代码。以下是 type="module"
使用的主要场景:
- 模块化开发:
- 当你的项目采用了ES6模块化机制,通过
import
和export
语句导入和导出模块时,需要在引用这些模块的<script>
标签中设置type="module"
。
- 当你的项目采用了ES6模块化机制,通过
- 异步加载与依赖管理:
- ES6模块支持异步加载,浏览器会并行加载多个模块,然后根据模块间的依赖关系按序执行。
- 这种方式可以避免传统的脚本阻塞页面渲染,提升页面加载性能。
- 代码组织与复用:
- 随着项目规模扩大,将代码拆分成多个模块进行管理和复用是非常必要的。
type="module"
允许开发者定义独立的、可维护的模块,并确保每个模块有自己独立的作用域。
- 随着项目规模扩大,将代码拆分成多个模块进行管理和复用是非常必要的。
- 避免全局命名空间污染:
- 在模块内部定义的变量、函数等不会自动添加到全局作用域,这有助于减少不同模块之间的命名冲突问题。
- 现代前端框架配合:
- 现代前端框架如Vue.js、React.js等,在构建工具配置下通常默认采用模块化开发,即便在实际应用中不直接写
<script type="module">
,但在构建后的产物或运行环境支持模块化的现代浏览器上,依然受益于模块化机制。
- 现代前端框架如Vue.js、React.js等,在构建工具配置下通常默认采用模块化开发,即便在实际应用中不直接写
- 跨域限制:
- 注意,当在本地文件系统(file://)上直接打开带有
type="module"
的HTML文件时,由于浏览器安全策略,默认不允许跨域请求本地文件,因此可能无法正常加载模块。解决办法是使用像VSCode的Live Server插件或者部署到支持HTTP协议的本地服务器环境来预览和调试。
- 注意,当在本地文件系统(file://)上直接打开带有
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>3D Map with Three.js</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "../static/latest/build/three.module.js",
"three/addons/": "../static/latest/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// //场景
const scene = new THREE.Scene();
//透视投影相机
const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);
//创建渲染器 设置抗锯齿属性为true
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
//作为元素添加到html中
document.body.appendChild(renderer.domElement);
camera.position.z = 5;
// resize 事件
window.addEventListener("resize", () => {
let width = window.innerWidth;
let height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
/**
* updateProjectionMatrix() 是 Three.js 中的一个方法,
* 通常用于相机(Camera)对象。在Three.js中,当你更改了相机的投影参数(如透视相机的视场角、近裁剪面或远裁剪面等),
* 或者更改了相机的位置、朝向等影响其投影矩阵的因素时,需要调用此方法来更新相机的内部投影矩阵。
*/
camera.updateProjectionMatrix();
});
// 定义旋转速度
const clockwiseRotationSpeed = -0.005; // 顺时针旋转速度
const counterclockwiseRotationSpeed = 0.005; // 逆时针旋转速度
// 创建虚线圆圈的函数(这里简化为8段虚线)
function createDashedCircle(radius, segments, dashSize, gapSize) {
const vertices = [];
const indices = [];
for (let i = 0; i <= segments * 4; i++) { // 每个点分割为dashSize和gapSize两部分
const angle = i / (segments * 4) * Math.PI * 2;
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
if (i % 4 === 0) {
vertices.push(x, y, 0); // dash起点
vertices.push(x + dashSize, y, 0); // dash终点
if (i > 0 && i !== segments * 12) { // 添加索引以形成线条
indices.push(i - 4, i - 3, i - 2, i - 1);
}
} else if (i % 4 === 2) {
vertices.push(x + dashSize + gapSize, y, 0); // gap终点同时也是下一个dash的起点
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setIndex(indices);
const material = new THREE.LineBasicMaterial({ color: 0x00ff00 });
return new THREE.LineSegments(geometry, material);
}
// 创建并添加两个虚线圆圈到场景
const outerDashedCircle = createDashedCircle(3, 120, 0.01, 0.01);
outerDashedCircle.position.z = -1;
scene.add(outerDashedCircle);
const innerDashedCircle = createDashedCircle(2.5, 120, 0.01, 0.01);
innerDashedCircle.position.z = -2;
scene.add(innerDashedCircle);
// 创建并初始化OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 设置OrbitControls的一些参数,例如只允许沿着X轴旋转
controls.enableDamping = true; // 使动画更平滑
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false; // 禁止屏幕空间平移
controls.minDistance = 2;
controls.maxDistance = 10;
//设置横向和纵向可旋转角度范围 可以通过尝试并调整
controls.maxAzimuthAngle = (45*Math.PI)/100
controls.minAzimuthAngle = (0*Math.PI)/100;
controls.maxPolarAngle = (90*Math.PI)/100
controls.minPolarAngle = (0*Math.PI)/100
controls.update()
// 环境光
/**
* AmbientLight 是 Three.js 中的一种光源类型,
* 它模拟环境光的效果,即场景中的每个点都会受到相同强度和颜色的光照。
* 在三维场景中添加 AmbientLight 可以提供全局的基础照明。
* @type {AmbientLight}
*/
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// const helper = new THREE.CameraHelper(camera)
// scene.add(helper)
// 每帧更新场景和控制
function animate() {
requestAnimationFrame(animate);
controls.update(); // 更新OrbitControls的状态
outerDashedCircle.rotation.z += clockwiseRotationSpeed;
innerDashedCircle.rotation.z += counterclockwiseRotationSpeed;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
整体代码结构跟上篇文章介绍的差不多,中间多了OrbitControls的引入和配置,再就是虚线双环的实现。
最终效果
threejs实现的双圆环效果图
问题记录
-
控制台报错: Uncaught SyntaxError: Cannot use import statement outside a module
这个问题主要是在script中引入了模块化的组件,但是没有配置参数type=“module”
解决方案就是:
<script type="module"> ... </script>
-
控制台报错:
Refused to execute script from 'http://127.0.0.1:5500/demo/static/latest/build/three.module.js' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.
circle_r159_module.html:1 Refused to execute script from 'http://127.0.0.1:5500/demo/static/latest/examples/jsm/controls/OrbitControls.js' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.
circle_r159_module.html:36 Uncaught ReferenceError: THREE is not defined
at circle_r159_module.html:36:23
这个问题就是上面提到的模块化实现直接在html中引用导致的问题,解决方案如上文所述,推荐使用importMap
解决。
<script type="importmap">
{
"imports": {
"three": "../static/latest/build/three.module.js",
"three/addons/": "../static/latest/examples/jsm/"
}
}
</script>
同时也要注意在引用的时候script标签添加type=”module“属性,然后引用的时候使用import
关键字即可。
-
控制台报错找不到js文件等问题
从以下几个方向检查:
- 检查
路径是否正确
,路径下是否存在目标文件 - 检查路径是否包含
中文字符
- 检查路径是否包含
.
等特殊字符
- 检查
总结
本文记录了在不依赖vue等模块化开发框架的基础上实现OrbitControls.js的引入过程,解决了模块化组件引入的问题,希望能帮助到需要的朋友。
针对以上问题有任何问题或者建议欢迎留言交流。