Atlaslog
Atlaslog / Examples / three.js / Model Rotate
three.js · Runnable Example

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.

Open standalone → Chinese notes All three.js examples
/demos/three-model-rotate.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/three-model-rotate.html
<!--
  说明:把代码拼出的建筑模型放进一个 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.

More three.js examples

three.js · EXAMPLEMulti Camera three.js · EXAMPLEMultiple Elements three.js · EXAMPLEMultiple Views