ArcballControls
是 Three.js 中的一个相机控制器,用于在 3D 场景中实现类似于鼠标拖拽的交互操作。它允许用户通过鼠标拖拽来旋转、平移和缩放相机,从而改变视角和观察场景中的物体。ArcballControls 可以帮助用户更直观地与 Three.js 场景进行交互,提供了简单而有效的控制方式,使用户能够更好地浏览和查看 3D 场景中的内容。
ArcballControls
的构造函数通常接受三个参数:
camera
:THREE.Camera 对象,表示要控制的相机。domElement
:HTML 元素,表示用于监听用户输入事件(如鼠标拖拽)的 DOM 元素。scene
:THREE.Scene 对象,表示当前的场景。
出参:ArcballControls 实例,用于控制相机的交互操作。
// 定义相机类型数组和相机类型对象
const cameras = ['Orthographic', 'Perspective'];
const cameraType = { type: 'Perspective' };
// 初始化透视相机距离和正交相机距离
const perspectiveDistance = 2.5;
const orthographicDistance = 120;
// 声明变量
let camera, controls, scene, renderer, gui;
let folderOptions, folderAnimations;
// Arcball 相机控制器 GUI
const arcballGui = {
gizmoVisible: true,
// 设置 Arcball 控制器
setArcballControls: function() {
controls = new ArcballControls(camera, renderer.domElement, scene);
controls.addEventListener('change', render);
this.gizmoVisible = true;
this.populateGui();
},
// 填充 GUI
populateGui: function() {
folderOptions.add(controls, 'enabled').name('Enable controls');
folderOptions.add(controls, 'enableGrid').name('Enable Grid');
folderOptions.add(controls, 'enableRotate').name('Enable rotate');
folderOptions.add(controls, 'enablePan').name('Enable pan');
folderOptions.add(controls, 'enableZoom').name('Enable zoom');
folderOptions.add(controls, 'cursorZoom').name('Cursor zoom');
folderOptions.add(controls, 'adjustNearFar').name('adjust near/far');
folderOptions.add(controls, 'scaleFactor', 1.1, 10, 0.1).name('Scale factor');
folderOptions.add(controls, 'minDistance', 0, 50, 0.5).name('Min distance');
folderOptions.add(controls, 'maxDistance', 0, 50, 0.5).name('Max distance');
folderOptions.add(controls, 'minZoom', 0, 50, 0.5).name('Min zoom');
folderOptions.add(controls, 'maxZoom', 0, 50, 0.5).name('Max zoom');
folderOptions.add(arcballGui, 'gizmoVisible').name('Show gizmos').onChange(function() {
controls.setGizmosVisible(arcballGui.gizmoVisible);
});
folderOptions.add(controls, 'copyState').name('Copy state(ctrl+c)');
folderOptions.add(controls, 'pasteState').name('Paste state(ctrl+v)');
folderOptions.add(controls, 'reset').name('Reset');
folderAnimations.add(controls, 'enableAnimations').name('Enable anim.');
folderAnimations.add(controls, 'dampingFactor', 0, 100, 1).name('Damping');
folderAnimations.add(controls, 'wMax', 0, 100, 1).name('Angular spd');
}
};
// 初始化函数
init();
function init() {
const container = document.createElement('div');
document.body.appendChild(container);
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 3;
renderer.domElement.style.background = 'linear-gradient( 180deg, rgba( 0,0,0,1 ) 0%, rgba( 128,128,255,1 ) 100% )';
container.appendChild(renderer.domElement);
// 创建场景
scene = new THREE.Scene();
// 创建透视相机并设置位置
camera = makePerspectiveCamera();
camera.position.set(0, 0, perspectiveDistance);
// 加载模型并设置材质
const material = new THREE.MeshStandardMaterial();
new OBJLoader()
.setPath('models/obj/cerberus/')
.load('Cerberus.obj', function(group) {
// 设置材质贴图
const textureLoader = new THREE.TextureLoader().setPath('models/obj/cerberus/');
material.roughness = 1;
material.metalness = 1;
const diffuseMap = textureLoader.load('Cerberus_A.jpg', render);
diffuseMap.colorSpace = THREE.SRGBColorSpace;
material.map = diffuseMap;
material.metalnessMap = material.roughnessMap = textureLoader.load('Cerberus_RM.jpg', render);
material.normalMap = textureLoader.load('Cerberus_N.jpg', render);
material.map.wrapS = THREE.RepeatWrapping;
material.roughnessMap.wrapS = THREE.RepeatWrapping;
material.metalnessMap.wrapS = THREE.RepeatWrapping;
material.normalMap.wrapS = THREE.RepeatWrapping;
// 遍历模型的子对象并应用材质
group.traverse(function(child) {
if (child.isMesh) {
child.material = material;
}
});
// 调整模型位置并添加到场景中
group.rotation.y = Math.PI / 2;
group.position.x += 0.25;
scene.add(group);
render();
// 加载 HDR 环境贴图
new RGBELoader()
.setPath('textures/equirectangular/')
.load('venice_sunset_1k.hdr', function(hdrEquirect) {
hdrEquirect.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = hdrEquirect;
render();
});
// 监听键盘事件和窗口大小变化事件
window.addEventListener('keydown', onKeyDown);
window.addEventListener('resize', onWindowResize);
// 创建 GUI
gui = new GUI();
gui.add(cameraType, 'type', cameras).name('Choose Camera').onChange(function() {
setCamera(cameraType.type);
});
folderOptions = gui.addFolder('Arcball parameters');
folderAnimations = folderOptions.addFolder('Animations');
// 设置 Arcball 控制器和 GUI
arcballGui.setArcballControls();
render();
});
}
// 创建正交相机
function makeOrthographicCamera() {
const halfFovV = THREE.MathUtils.DEG2RAD * 45 * 0.5;
const halfFovH = Math.atan((window.innerWidth / window.innerHeight) * Math.tan(halfFovV));
const halfW = perspectiveDistance * Math.tan(halfFovH);
const halfH = perspectiveDistance * Math.tan(halfFovV);
const near = 0.01;
const far = 2000;
const newCamera = new THREE.OrthographicCamera(-halfW, halfW, halfH, -halfH, near, far);
return newCamera;
}
// 创建透视相机
function makePerspectiveCamera() {
const fov = 45;
const aspect = window.innerWidth / window.innerHeight;
const near = 0.01;
const far = 2000;
const newCamera = new THREE.PerspectiveCamera(fov, aspect, near, far);
return newCamera;
}
// 当窗口大小变化时调整相机参数和渲染器大小
function onWindowResize() {
if (camera.type == 'OrthographicCamera') {
const halfFovV = THREE.MathUtils.DEG2RAD * 45 * 0.5;
const halfFovH = Math.atan((window.innerWidth / window.innerHeight) * Math.tan(halfFovV));
const halfW = perspectiveDistance * Math.tan(halfFovH);
const halfH = perspectiveDistance * Math.tan(halfFovV);
camera.left = -halfW;
camera.right = halfW;
camera.top = halfH;
camera.bottom = -halfH;
} else if (camera.type == 'PerspectiveCamera') {
camera.aspect = window.innerWidth / window.innerHeight;
}
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
render();
}
// 渲染函数
function render() {
renderer.render(scene, camera);
}
// 监听键盘按下事件
function onKeyDown(event) {
if (event.key === 'c') {
if (event.ctrlKey || event.metaKey) {
controls.copyState();
}
} else if (event.key === 'v') {
if (event.ctrlKey || event.metaKey) {
controls.pasteState();
}
}
}
// 切换相机类型并更新相机和控制器
function setCamera(type) {
if (type == 'Orthographic') {
camera = makeOrthographicCamera();
camera.position.set(0, 0, orthographicDistance);
} else if (type == 'Perspective') {
camera = makePerspectiveCamera();
camera.position.set(0, 0, perspectiveDistance);
}
controls.setCamera(camera);
render();
}
全部源码
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - arcball controls</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - arcball controls<br/>
<a href="http://www.polycount.com/forum/showthread.php?t=130641" target="_blank" rel="noopener">Cerberus(FFVII Gun) model</a> by Andrew Maximov.
</div>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { ArcballControls } from 'three/addons/controls/ArcballControls.js';
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const cameras = [ 'Orthographic', 'Perspective' ];
const cameraType = { type: 'Perspective' };
const perspectiveDistance = 2.5;
const orthographicDistance = 120;
let camera, controls, scene, renderer, gui;
let folderOptions, folderAnimations;
const arcballGui = {
gizmoVisible: true,
setArcballControls: function () {
controls = new ArcballControls( camera, renderer.domElement, scene );
controls.addEventListener( 'change', render );
this.gizmoVisible = true;
this.populateGui();
},
populateGui: function () {
folderOptions.add( controls, 'enabled' ).name( 'Enable controls' );
folderOptions.add( controls, 'enableGrid' ).name( 'Enable Grid' );
folderOptions.add( controls, 'enableRotate' ).name( 'Enable rotate' );
folderOptions.add( controls, 'enablePan' ).name( 'Enable pan' );
folderOptions.add( controls, 'enableZoom' ).name( 'Enable zoom' );
folderOptions.add( controls, 'cursorZoom' ).name( 'Cursor zoom' );
folderOptions.add( controls, 'adjustNearFar' ).name( 'adjust near/far' );
folderOptions.add( controls, 'scaleFactor', 1.1, 10, 0.1 ).name( 'Scale factor' );
folderOptions.add( controls, 'minDistance', 0, 50, 0.5 ).name( 'Min distance' );
folderOptions.add( controls, 'maxDistance', 0, 50, 0.5 ).name( 'Max distance' );
folderOptions.add( controls, 'minZoom', 0, 50, 0.5 ).name( 'Min zoom' );
folderOptions.add( controls, 'maxZoom', 0, 50, 0.5 ).name( 'Max zoom' );
folderOptions.add( arcballGui, 'gizmoVisible' ).name( 'Show gizmos' ).onChange( function () {
controls.setGizmosVisible( arcballGui.gizmoVisible );
} );
folderOptions.add( controls, 'copyState' ).name( 'Copy state(ctrl+c)' );
folderOptions.add( controls, 'pasteState' ).name( 'Paste state(ctrl+v)' );
folderOptions.add( controls, 'reset' ).name( 'Reset' );
folderAnimations.add( controls, 'enableAnimations' ).name( 'Enable anim.' );
folderAnimations.add( controls, 'dampingFactor', 0, 100, 1 ).name( 'Damping' );
folderAnimations.add( controls, 'wMax', 0, 100, 1 ).name( 'Angular spd' );
}
};
init();
function init() {
const container = document.createElement( 'div' );
document.body.appendChild( container );
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 3;
renderer.domElement.style.background = 'linear-gradient( 180deg, rgba( 0,0,0,1 ) 0%, rgba( 128,128,255,1 ) 100% )';
container.appendChild( renderer.domElement );
//
scene = new THREE.Scene();
camera = makePerspectiveCamera();
camera.position.set( 0, 0, perspectiveDistance );
const material = new THREE.MeshStandardMaterial();
new OBJLoader()
.setPath( 'models/obj/cerberus/' )
.load( 'Cerberus.obj', function ( group ) {
const textureLoader = new THREE.TextureLoader().setPath( 'models/obj/cerberus/' );
material.roughness = 1;
material.metalness = 1;
const diffuseMap = textureLoader.load( 'Cerberus_A.jpg', render );
diffuseMap.colorSpace = THREE.SRGBColorSpace;
material.map = diffuseMap;
material.metalnessMap = material.roughnessMap = textureLoader.load( 'Cerberus_RM.jpg', render );
material.normalMap = textureLoader.load( 'Cerberus_N.jpg', render );
material.map.wrapS = THREE.RepeatWrapping;
material.roughnessMap.wrapS = THREE.RepeatWrapping;
material.metalnessMap.wrapS = THREE.RepeatWrapping;
material.normalMap.wrapS = THREE.RepeatWrapping;
group.traverse( function ( child ) {
if ( child.isMesh ) {
child.material = material;
}
} );
group.rotation.y = Math.PI / 2;
group.position.x += 0.25;
scene.add( group );
render();
new RGBELoader()
.setPath( 'textures/equirectangular/' )
.load( 'venice_sunset_1k.hdr', function ( hdrEquirect ) {
hdrEquirect.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = hdrEquirect;
render();
} );
window.addEventListener( 'keydown', onKeyDown );
window.addEventListener( 'resize', onWindowResize );
//
gui = new GUI();
gui.add( cameraType, 'type', cameras ).name( 'Choose Camera' ).onChange( function () {
setCamera( cameraType.type );
} );
folderOptions = gui.addFolder( 'Arcball parameters' );
folderAnimations = folderOptions.addFolder( 'Animations' );
arcballGui.setArcballControls();
render();
} );
}
function makeOrthographicCamera() {
const halfFovV = THREE.MathUtils.DEG2RAD * 45 * 0.5;
const halfFovH = Math.atan( ( window.innerWidth / window.innerHeight ) * Math.tan( halfFovV ) );
const halfW = perspectiveDistance * Math.tan( halfFovH );
const halfH = perspectiveDistance * Math.tan( halfFovV );
const near = 0.01;
const far = 2000;
const newCamera = new THREE.OrthographicCamera( - halfW, halfW, halfH, - halfH, near, far );
return newCamera;
}
function makePerspectiveCamera() {
const fov = 45;
const aspect = window.innerWidth / window.innerHeight;
const near = 0.01;
const far = 2000;
const newCamera = new THREE.PerspectiveCamera( fov, aspect, near, far );
return newCamera;
}
function onWindowResize() {
if ( camera.type == 'OrthographicCamera' ) {
const halfFovV = THREE.MathUtils.DEG2RAD * 45 * 0.5;
const halfFovH = Math.atan( ( window.innerWidth / window.innerHeight ) * Math.tan( halfFovV ) );
const halfW = perspectiveDistance * Math.tan( halfFovH );
const halfH = perspectiveDistance * Math.tan( halfFovV );
camera.left = - halfW;
camera.right = halfW;
camera.top = halfH;
camera.bottom = - halfH;
} else if ( camera.type == 'PerspectiveCamera' ) {
camera.aspect = window.innerWidth / window.innerHeight;
}
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
render();
}
function render() {
renderer.render( scene, camera );
}
function onKeyDown( event ) {
if ( event.key === 'c' ) {
if ( event.ctrlKey || event.metaKey ) {
controls.copyState();
}
} else if ( event.key === 'v' ) {
if ( event.ctrlKey || event.metaKey ) {
controls.pasteState();
}
}
}
function setCamera( type ) {
if ( type == 'Orthographic' ) {
camera = makeOrthographicCamera();
camera.position.set( 0, 0, orthographicDistance );
} else if ( type == 'Perspective' ) {
camera = makePerspectiveCamera();
camera.position.set( 0, 0, perspectiveDistance );
}
controls.setCamera( camera );
render();
}
</script>
</body>
</html>