Controls Fly
Runnable three.js example: Controls Fly. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
说明:飞行控制用键盘推动相机,鼠标拖拽改变朝向,适合在空中巡视大范围场景
核心逻辑:FlyControls 每帧接收 delta,键盘持续输入才有速度
依赖:three.js 0.160.0、FlyControls · jsDelivr CDN(ESM + importmap)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>飞行控制</title>
<style>
html,body{height:100%;margin:0;background:#081018}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#e6e8ec}
.bar{flex:0 0 auto;padding:8px 12px;background:#161b22;border-bottom:1px solid #252c36;
display:flex;gap:14px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
.bar b{color:#58a6ff;font-weight:600;font-variant-numeric:tabular-nums}
.bar input[type=range]{width:150px;accent-color:#4f9ef8}
.bar .sp{flex:1 1 auto}
#stage{flex:1 1 auto;min-height:0;position:relative;cursor:grab}
#stage canvas{display:block}
#keys{position:absolute;left:12px;bottom:12px;padding:10px 14px;border-radius:6px;
background:rgba(10,16,22,.8);border:1px solid #223040;font-size:12px;line-height:1.9;
color:#b9c6d4;pointer-events:none}
#keys kbd{display:inline-block;min-width:16px;padding:0 5px;margin-right:4px;border-radius:3px;
background:#1d2833;border:1px solid #324050;color:#dbe6f2;font-size:11px;text-align:center}
</style>
</head>
<body>
<div class="bar">
<span>速度 <b id="spdv">40</b> 单位/秒</span>
<label>调速 <input type="range" id="spd" min="5" max="120" step="5" value="40"></label>
<span class="sp"></span>
<span>点击画布获得键盘焦点</span>
</div>
<div id="stage">
<div id="keys">
<kbd>W</kbd><kbd>S</kbd> 前后 <kbd>A</kbd><kbd>D</kbd> 左右<br>
<kbd>R</kbd><kbd>F</kbd> 升降 拖拽鼠标转向<br>
<kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> 俯仰与翻滚
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { FlyControls } from 'three/addons/controls/FlyControls.js';
const stage = document.getElementById('stage');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x081018);
scene.fog = new THREE.Fog(0x081018, 80, 220);
const camera = new THREE.PerspectiveCamera(55, stage.clientWidth / stage.clientHeight, 0.5, 600);
camera.position.set(0, 30, 80);
const controls = new FlyControls(camera, renderer.domElement);
controls.movementSpeed = 40;
controls.rollSpeed = 0.6;
controls.autoForward = false;
controls.dragToLook = true;
scene.add(new THREE.HemisphereLight(0xb8d4f0, 0x101a22, 1.35));
const sun = new THREE.DirectionalLight(0xffeacb, 2.2);
sun.position.set(-60, 90, 40);
scene.add(sun);
/* 地面:一张方格网,方便判断飞行速度与高度 */
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(600, 600),
new THREE.MeshStandardMaterial({ color: 0x0f1820, roughness: 1 })
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
const grid = new THREE.GridHelper(600, 60, 0x24384a, 0x182633);
grid.position.y = 0.05;
scene.add(grid);
/* 两组塔楼:一组沿路排列,一组在高处形成航标 */
function hash(n) {
const x = Math.sin(n * 15.7 + 3.3) * 43758.5453;
return x - Math.floor(x);
}
for (let i = 0; i < 90; i++) {
const h = 8 + Math.pow(hash(i), 1.6) * 70;
const w = 3 + hash(i + 3) * 5;
const x = (hash(i + 7) - 0.5) * 320;
const z = (hash(i + 11) - 0.5) * 320;
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.56 - hash(i + 5) * 0.1, 0.22, 0.34 + hash(i + 9) * 0.2),
roughness: 0.55,
metalness: 0.2
});
const b = new THREE.Mesh(new THREE.BoxGeometry(w, h, w), mat);
b.position.set(x, h / 2, z);
scene.add(b);
/* 顶部一盏小灯,远处也能看到塔楼位置 */
if (i % 3 === 0) {
const lamp = new THREE.Mesh(
new THREE.SphereGeometry(0.7, 8, 6),
new THREE.MeshBasicMaterial({ color: 0xff7b72 })
);
lamp.position.set(x, h + 0.8, z);
scene.add(lamp);
}
}
/* 松开鼠标不会继续飞,但键盘输入需要画布持有焦点 */
renderer.domElement.addEventListener('pointerdown', function () {
renderer.domElement.focus();
});
const spdEl = document.getElementById('spd');
const spdvEl = document.getElementById('spdv');
spdEl.addEventListener('input', function () {
const v = Number(spdEl.value);
controls.movementSpeed = v;
spdvEl.textContent = String(v);
});
const clock = new THREE.Clock();
renderer.setAnimationLoop(function () {
const dt = Math.min(clock.getDelta(), 0.1);
controls.update(dt);
renderer.render(scene, camera);
});
window.addEventListener('resize', function () {
const w = stage.clientWidth;
const h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.