THREEJS:由鼠标控制的空间物体的运动
在threejs中,物体的转动并不象人们想象中的方式去进行.用一个汽车来举例.当一辆汽车转弯后,应该是按照转弯后的方向去前进,可是在threejs中,汽车转弯后,是侧着身子沿着原来的方向在运动.因为在threejs中,物体的运动都是以其所在的坐标系为参照的.物体的运动并不影响其所在的坐标系的姿态.所以汽车的运动并不能被真实地反映出来.
汽车的运动只是个平面运动,相对于汽车来说,在三维空间的飞机的运动就更复杂一些.同样,在三维空间中,也存在类似的情况.
这种情况,并不是简单地改变物体所在的坐标系就能解决问题的.因为物体的旋转会造成其在世界坐标系下的位置发生变化,而这种变化会因为物体所在的坐标系的变换造成累加.解决的办法就是,在移动物体的父坐标系时, 按照单位长度投影分量进行移动,变换后,坐标系内的物体整体进行移动.而旋转则不受影响.
// 旋转物体的父坐标系 (180,表示每次旋转1度)
function Rotating(xdir,ydir,zdir) {
localSystem1.rotation.x += xdir * Math.PI/180;
localSystem1.rotation.y += ydir * Math.PI/180;
localSystem1.rotation.z += zdir * Math.PI/180;
// console.log('Rotating:',localSystem1.rotation)
}
// 移动物体的父坐标系, 按照单位长度投影分量进行移动
// 变换后,坐标系内的物体整体进行移动(0.1表示每次移动的长度)
function Moving(xdir,ydir,zdir) {
let sPoint = new THREE.Vector3(0.1*xdir,0.1*ydir,0.1*zdir);
let nPoint = LocalToWorld(localSystem1,sPoint);
localSystem1.position.x = nPoint.x;
localSystem1.position.y = nPoint.y;
localSystem1.position.z = nPoint.z;
// console.log('Moving:',localSystem1.position)
}
以下是对本文所附程序的简单说明:
1,建立一个子坐标系localSystem1,物体(mesh)即建立在这个坐标系中(表现为一个三角形);
2,空间的各种运动形式均通过按键来模拟.具体如下:
位移:
X方向 q左移/右移7 [5,-5,0]
Y方向 w上升/下降8 [3,-3,0]
Z方向 e前进/后退9 [5,-5,1]
旋转:
X方向 a上翻/下翻4 [0.3,-0.3,0]
Y方向 s左转/右转5 [0.5,-0.5,0]
Z方向 d左滚/右滚6 [-0.3,0.3,0]
加速: 按正向键为加速,直到极限值
减速: 按反向键为减速,直到极限值
SPACEBAR: 开始/暂停
3,在交互菜单里选择"orbit",可以显示端点的运动轨迹.这对于理解点的空间运动很有帮助.
4,程序中假定Z轴正向为前进方向,且符合右手定则.所以有以下的对应关系:
俯仰角:X轴
偏航角:Y轴
翻滚角:Z轴
以下为原程序:
<template>
<div id="info1"><pre>
X q左移/7右移
Y w上升/8下降
Z e前进/9后退
X: a上翻/4下翻
Y: s左转/5右转
Z: d左滚/6右滚
</pre></div>
</template>
<script type="module">
// 跟踪点在子坐标系中的移动轨迹
// 将点设置为实体Points对象,随着子坐标系的变换,点的坐标随之变换,再转换到世界坐标系中,从而描绘出轨迹
// 不论是物体移动还是坐标系移动,都可以绘出物体移动的轨迹 (以上假定Z轴正向为前进方向.)
// Y 位移:
// ^ X q左移/右移7
// | ^ Z Y w上升/下降8
// | / Z e前进/后退9
// | / 旋转:
// | / X: a上翻/下翻4
// X <--------|------------ Y: s左转/右转5
// /| Z: d左滚/右滚6
// / | 加速: 按正向键为加速,直到极限值
// / | 减速: 按反向键为减速,直到极限值
// / | SPACEBAR: 开始/暂停
// Gu Laicheng , 2022-04-26
import * as THREE from 'three';
import Stats from 'three/examples/jsm/libs/stats.module.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
// 导入控制器,轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import 'default-passive-events'
let camera, scene, renderer, stats;
var isMouseDown = false;
let mesh, material, geometry, texture;
let axesHelper0,axesHelper1 ;
let localSystem1 ;
let gridHelper, orbitControls;
var time = 0;
var xdir=false, ydir=false, zdir=false;
var I=0;
var director=1;
var LineIDs=[];
let point1 , point2;
let WorldDirection= new THREE.Vector3();
const controls = new function () {
this.showAxes = true;
this.showGrid = false;
this.orbit = false;
initScene();
draw();
initGui();
animate();
function initScene() {
camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 1, 3500 );
camera.position.x = 125;
camera.position.y = 55;
camera.position.z = 350;
scene = new THREE.Scene();
scene.background = new THREE.Color( 0x050505 );
scene.autoUpdate = true; // 默认值为true,若设置了这个值,则渲染器会检查每一帧是否需要更新场景及其中物体的矩阵。 当设为false时,你必须亲自手动维护场景中的矩阵。
const light = new THREE.HemisphereLight();
scene.add( light );
// 定义多个坐标系统:
axesHelper0 = new THREE.AxesHelper(35);
axesHelper1 = new THREE.AxesHelper(25);
scene.add( axesHelper0 )
localSystem1 = new THREE.Object3D()
scene.add(localSystem1)
localSystem1.add(axesHelper1)
localSystem1.position.set(0,10,0);
localSystem1.rotation.x = Math.PI/3;
// 下以更新世界坐标的动作在render时自动执行,在render之前需要手工执行一次.
scene.updateMatrixWorld(); // 这个动作是自动嵌套(递归)执行的 ???
//localSystem1.updateMatrixWorld();
gridHelper = new THREE.GridHelper(100, 100, 0x5C5C5C, 0x888888 );
// scene.add(gridHelper);
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
orbitControls = new OrbitControls(camera, renderer.domElement);
//设置控制器的中心点
//orbitControls.target.set( 0, 5, 0 );
// 如果使用animate方法时,将此函数删除
//orbitControls.addEventListener( 'change', render );
// 使动画循环使用时阻尼或自转 意思是否有惯性
orbitControls.enableDamping = true;
//动态阻尼系数 就是鼠标拖拽旋转灵敏度
//orbitControls.dampingFactor = 0.25;
//是否可以缩放
orbitControls.enableZoom = true;
//是否自动旋转
orbitControls.autoRotate = true;
orbitControls.autoRotateSpeed = 0.5;
//设置相机距离原点的最远距离
orbitControls.minDistance = 1;
//设置相机距离原点的最远距离
orbitControls.maxDistance = 2000;
//是否开启右键拖拽
orbitControls.enablePan = true;
stats = new Stats();
document.body.appendChild( stats.dom );
geometry = new THREE.BufferGeometry();
window.addEventListener( 'resize', onWindowResize );
window.addEventListener( 'keydown', function(event) { //KeyBoardEvent
console.log("key:",event.key,',code:',event.code)
if ( event.key === ' ') {// SPACEBAR
isMouseDown = !isMouseDown;
} else if ( event.key === 'a') { Rotating(1,0,0);
} else if ( event.key === 's') { Rotating(0,1,0);
} else if ( event.key === 'd') { Rotating(0,0,1);
} else if ( event.key === '4') { Rotating(-1,0,0);
} else if ( event.key === '5') { Rotating(0,-1,0);
} else if ( event.key === '6') { Rotating(0,0,-1);
} else if ( event.key === 'q') { Moving(1,0,0);
} else if ( event.key === 'w') { Moving(0,1,0);
} else if ( event.key === 'e') { Moving(0,0,1);
} else if ( event.key === '7') { Moving(-1,0,0);
} else if ( event.key === '8') { Moving(0,-1,0);
} else if ( event.key === '9') { Moving(0,0,-1);
} else if ( event.key === 'v') {
console.log('q1:',mesh.quaternion, mesh.rotation)
rotateEndPoint = new THREE.Vector3(-1, 0, 0); // projectOnTrackball(0.1, 0.1);
curQuaternion = mesh.quaternion;
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), Math.PI / 12 );
quaternion.setFromAxisAngle( new THREE.Vector3( 1, 0, 0 ), Math.PI / 2 );
mesh.applyQuaternion( quaternion );
console.log('q2:',mesh.quaternion, mesh.rotation)
} else if(event.key == 'ArrowLeft') {
director = 1;
} else if(event.key == 'ArrowUp') {
director = 1;
} else if(event.key == 'ArrowRight') {
director = -1;
} else if(event.key == 'ArrowDown') {
director = -1;
} else if(event.key == '/') {
mesh.getWorldDirection(WorldDirection);
console.log('position :',mesh.position);
console.log('rotation :',mesh.rotation);
console.log('scale :',mesh.scale );
console.log('WorldDirection :',WorldDirection);
console.log('dir :',xdir,ydir,zdir);
// 键盘上下左右 方向键的键码(keyCode)是38、40、37和39
// key: ArrowUp ,code: ArrowUp
// key: ArrowDown ,code: ArrowDown
// key: ArrowLeft ,code: ArrowLeft
// key: ArrowRight ,code: ArrowRight
// window.addEventListener('mouseup', onMouseUp);
// 动态空间曲线
function draw(){
point1 = new THREE.Points();
point1.position.set(-10.0, -10.0, 10.0);
point2 = new THREE.Points();
point2.position.set( 10.0, -10.0, 10.0);
localSystem1.add(point1);
localSystem1.add(point2);
triangle(localSystem1);
// triangle
function triangle(s) {
geometry = new THREE.BufferGeometry();
const vertices = new Float32Array( [
-10.0, -10.0, 10.0,
10.0, -10.0, 10.0,
10.0, 10.0, 10.0,
// 1.0, 1.0, 1.0,
// -1.0, 1.0, 1.0,
// -1.0, -1.0, 1.0
var colors = [
1,0,0,
0,1,0,
0,0,1,
// itemSize = 3 因为每个顶点都是一个三元组。
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
material = new THREE.MeshBasicMaterial( {
// color: 0xff0000,
wireframe: true,
vertexColors: true } );
mesh = new THREE.Mesh( geometry, material );
s.add(mesh);
// line
function lines(s,points,c) {
const material = new THREE.LineBasicMaterial({
color: c
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const mesh = new THREE.Line( geometry, material );
s.add( mesh );
return mesh.id;
function initGui() {
const gui = new GUI();
gui.add(controls, 'showAxes').onChange(()=>{
if (controls.showAxes) {
scene.add( axesHelper0 );
localSystem1.add( axesHelper1 ) ;
} else {
scene.remove( axesHelper0 )
localSystem1.remove( axesHelper1 ) ;
gui.add(controls, 'showGrid').onChange(()=>{
if (controls.showGrid) {
scene.add( gridHelper );
} else {
scene.remove( gridHelper );
gui.add(controls,"orbit");
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
function animate() {
if (controls.showAxes) {
scene.add( axesHelper0 );
localSystem1.add( axesHelper1 ) ;
} else {
scene.remove( axesHelper0 )
localSystem1.remove( axesHelper1 ) ;
if (controls.showGrid) {
scene.add( gridHelper );
} else {
scene.remove( gridHelper );
requestAnimationFrame( animate );
render();
stats.update();
var disp=true;
let LineID1,LineID2;
let wpoints1 = [] ,wpoints2 = [];
let geometry1,geometry2,colors = [];
function render() {
if (isMouseDown) {
if( time > 200 && time < 41000 && time%2 === 0 ) {
// rubber();
// if(controls.orbit) {
// orbit();
// } else {
// wpoints1 = [];
// wpoints2 = [];
// ====================================================================
if(disp) {
// console.log('position :',wp1);
// console.log('rotation :',mesh.rotation);
// console.log('scale :',mesh.scale );
disp=false;
time++;
renderer.render(scene, camera);
// 旋转物体的父坐标系
function Rotating(xdir,ydir,zdir) {
localSystem1.rotation.x += xdir * Math.PI/18;
localSystem1.rotation.y += ydir * Math.PI/18;
localSystem1.rotation.z += zdir * Math.PI/18;
rubber();
if(controls.orbit) {
orbit();
} else {
wpoints1 = [];
wpoints2 = [];
// 移动物体的父坐标系, 按照单位长度投影分量进行移动
// 变换后,坐标系内的物体整体进行移动
function Moving(xdir,ydir,zdir) {
let sPoint = new THREE.Vector3(0.5*xdir,0.5*ydir,0.5*zdir);
let nPoint = LocalToWorld(localSystem1,sPoint);
localSystem1.position.x = nPoint.x;
localSystem1.position.y = nPoint.y;
localSystem1.position.z = nPoint.z;
rubber();
if(controls.orbit) {
orbit();
} else {
wpoints1 = [];
wpoints2 = [];
function rubber() {
LineIDs.forEach((item)=>{purgeObj(item)});
LineIDs = [
lines(scene,[
LocalToWorld ( localSystem1, point1.position ),
new THREE.Vector3(0,0,0),
LocalToWorld ( localSystem1, point2.position ),
],0xFF00FF)
// ======== 绘制点的轨迹曲线 =========================================
function orbit() {
purgeObj(LineID1);
purgeObj(LineID2);
let wp1 = LocalToWorld (localSystem1, point1.position );
let wp2 = LocalToWorld (localSystem1, point2.position );
wpoints1.push(wp1);
wpoints2.push(wp2);
geometry1 = new THREE.BufferGeometry().setFromPoints( wpoints1 );
geometry2 = new THREE.BufferGeometry().setFromPoints( wpoints2 );
colors.push(0.6,0.6,0.6);
geometry1.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
geometry2.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
const material = new THREE.LineBasicMaterial({
// color: 0xFFFF00,
vertexColors: true
const mesh1 = new THREE.Line( geometry1, material );
scene.add(mesh1);
LineID1 = mesh1.id;
const mesh2 = new THREE.Line( geometry2, material );
scene.add(mesh2);
LineID2 = mesh2.id;
// console.log('orbit Line :',wpoints1, wpoints2)
// 为了避免 localToWorld 方法的怪异行为
// s: scene
// p: point
function LocalToWorld(s,p) {
let p0 = p.clone();
return s.localToWorld ( p0 );
function purgeObj(objid) {
if(!objid) return;
let obj = scene.getObjectById ( objid );
if(!obj) return;
const mater = obj.material;
const geome = obj.geometry;
scene.remove(obj);
mater.dispose();
geome.dispose();
</script>
<style scoped>
body {
margin: 0;
background-color: #000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
canvas {
width: 100%;
height: 100%;
display: block;
#widget select option {
background-color: #333333;
#info1 {
position: absolute;
background-color: rgba(0,0,0,0.2);
top: 0px;
left: 0px;
width: 40%;
height: 120px;
padding: 10px;
color: rgb(223, 209, 19);
box-sizing: border-box;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
/* opacity:0.2; */
z-index: 1; /* TODO Solve this in HTML */
#overlay {
position: absolute;
font-size: 16px;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;