Model Rotate
Runnable three.js example: Model Rotate. 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
<!--
说明:把代码拼出的建筑模型放进一个 Group,用拖拽直接旋转模型本身,而不是旋转相机
核心逻辑:指针位移累加到 group.rotation,绕自身原点旋转,滚轮只改变相机距离
依赖:three.js 0.160.0 · 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:#0d1117}
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:12px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
.bar b{color:#58a6ff;font-weight:600}
.bar .mono{font-family:ui-monospace,Consolas,monospace;color:#c8d2e0}
.bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:5px;cursor:pointer;
border:1px solid #303845;background:#1d2531;color:#c8d2e0}
.bar button:hover{background:#253040}
.bar .sp{flex:1 1 auto}
#stage{flex:1 1 auto;min-height:0;position:relative;cursor:grab}
#stage canvas{display:block}
#stage.dragging{cursor:grabbing}
</style>
</head>
<body>
<div class="bar">
<span>旋转 <b id="ry">0°</b> / 俯仰 <b id="rx">0°</b></span>
<button id="auto">停止自动旋转</button>
<button id="reset">重置</button>
<button id="wire">线框</button>
<span class="sp"></span>
<span>拖拽旋转模型 · 滚轮缩放</span>
</div>
<div id="stage"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
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;
renderer.toneMappingExposure = 1.05;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b1016);
scene.fog = new THREE.Fog(0x0b1016, 60, 140);
const camera = new THREE.PerspectiveCamera(
40, stage.clientWidth / stage.clientHeight, 0.5, 300
);
camera.position.set(30, 22, 34);
camera.lookAt(0, 7, 0);
scene.add(new THREE.HemisphereLight(0xbcd8ff, 0x16202a, 1.0));
const sun = new THREE.DirectionalLight(0xfff0d8, 2.4);
sun.position.set(-24, 36, 20);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -26;
sun.shadow.camera.right = 26;
sun.shadow.camera.top = 26;
sun.shadow.camera.bottom = -26;
sun.shadow.camera.near = 1;
sun.shadow.camera.far = 110;
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.normalBias = 0.04;
scene.add(sun);
const ground = new THREE.Mesh(
new THREE.CircleGeometry(40, 72),
new THREE.MeshStandardMaterial({ color: 0x141c24, roughness: 1 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
/* ---------- 用基本体拼一栋楼 ---------- */
const model = new THREE.Group();
model.name = 'model';
scene.add(model);
const shellMat = new THREE.MeshStandardMaterial({
color: 0x8ea3ba,
roughness: 0.62,
metalness: 0.18
});
const trimMat = new THREE.MeshStandardMaterial({
color: 0x3d4b5c,
roughness: 0.55,
metalness: 0.25
});
const glassMat = new THREE.MeshStandardMaterial({
color: 0x1b2a3a,
roughness: 0.18,
metalness: 0.55,
emissive: 0x1f5f8a,
emissiveIntensity: 1.1
});
function addBox(w, h, d, x, y, z, mat) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
mesh.position.set(x, y, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
model.add(mesh);
return mesh;
}
/* 底座与主体 */
addBox(14, 1.2, 14, 0, 0.6, 0, trimMat);
addBox(10, 12, 10, 0, 7, 0, shellMat);
/* 四个角柱,比主体略高,形成层次 */
[[-4.6, -4.6], [4.6, -4.6], [-4.6, 4.6], [4.6, 4.6]].forEach(function (p) {
addBox(1.2, 13.4, 1.2, p[0], 7.1, p[1], trimMat);
});
/* 玻璃幕墙条带 */
for (let i = 0; i < 4; i++) {
const y = 2.6 + i * 2.8;
addBox(10.06, 1.5, 10.06, 0, y, 0, glassMat);
}
/* 屋顶与天线 */
addBox(8, 0.8, 8, 0, 13.6, 0, trimMat);
const mast = new THREE.Mesh(
new THREE.CylinderGeometry(0.18, 0.28, 6, 10),
trimMat
);
mast.position.y = 17;
mast.castShadow = true;
model.add(mast);
/* 转盘:原地旋转的参照物 */
const turntable = new THREE.Mesh(
new THREE.CylinderGeometry(11, 11, 0.24, 64),
new THREE.MeshStandardMaterial({ color: 0x1c2632, roughness: 0.9 })
);
turntable.position.y = 0.12;
turntable.receiveShadow = true;
scene.add(turntable);
/* ---------- 交互 ---------- */
const ryEl = document.getElementById('ry');
const rxEl = document.getElementById('rx');
const autoBtn = document.getElementById('auto');
const resetBtn = document.getElementById('reset');
const wireBtn = document.getElementById('wire');
let auto = true;
let dragging = false;
let lastX = 0;
let lastY = 0;
let wire = false;
function readout() {
let degY = THREE.MathUtils.radToDeg(model.rotation.y) % 360;
if (degY < 0) degY += 360;
ryEl.textContent = degY.toFixed(0) + '°';
rxEl.textContent = THREE.MathUtils.radToDeg(model.rotation.x).toFixed(0) + '°';
}
const canvas = renderer.domElement;
canvas.addEventListener('pointerdown', function (event) {
dragging = true;
lastX = event.clientX;
lastY = event.clientY;
stage.classList.add('dragging');
canvas.setPointerCapture(event.pointerId);
});
canvas.addEventListener('pointermove', function (event) {
if (!dragging) return;
const dx = event.clientX - lastX;
const dy = event.clientY - lastY;
lastX = event.clientX;
lastY = event.clientY;
model.rotation.y += dx * 0.008;
model.rotation.x = THREE.MathUtils.clamp(
model.rotation.x + dy * 0.004, -0.22, 0.68
);
readout();
});
function endDrag(event) {
if (!dragging) return;
dragging = false;
stage.classList.remove('dragging');
if (canvas.hasPointerCapture(event.pointerId)) {
canvas.releasePointerCapture(event.pointerId);
}
}
canvas.addEventListener('pointerup', endDrag);
canvas.addEventListener('pointercancel', endDrag);
canvas.addEventListener('wheel', function (event) {
event.preventDefault();
const dir = new THREE.Vector3().subVectors(camera.position, new THREE.Vector3(0, 7, 0));
const len = THREE.MathUtils.clamp(dir.length() * (1 + event.deltaY * 0.0012), 18, 85);
dir.setLength(len);
camera.position.copy(new THREE.Vector3(0, 7, 0)).add(dir);
camera.lookAt(0, 7, 0);
}, { passive: false });
autoBtn.addEventListener('click', function () {
auto = !auto;
autoBtn.textContent = auto ? '停止自动旋转' : '开始自动旋转';
});
resetBtn.addEventListener('click', function () {
model.rotation.set(0, 0, 0);
camera.position.set(30, 22, 34);
camera.lookAt(0, 7, 0);
readout();
});
wireBtn.addEventListener('click', function () {
wire = !wire;
wireBtn.textContent = wire ? '实体' : '线框';
[shellMat, trimMat, glassMat].forEach(function (m) { m.wireframe = wire; });
});
renderer.setAnimationLoop(function () {
if (auto && !dragging) {
model.rotation.y += 0.0045;
readout();
}
renderer.render(scene, camera);
});
readout();
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.