Multiple Views
Runnable three.js example: Multiple Views. 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
<!--
说明:同一块画布切成四个视口,一个透视相机加三个正交相机,共用一次渲染循环
核心逻辑:setScissorTest / setViewport / setScissor 按象限铺开,逐相机渲染
依赖:three.js 0.160.0、OrbitControls · 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:#0a1016}
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}
.bar .sp{flex:1 1 auto}
#stage{flex:1 1 auto;min-height:0;position:relative}
#stage canvas{display:block}
#labels{position:absolute;inset:0;pointer-events:none}
#labels span{position:absolute;padding:3px 8px;border-radius:4px;font-size:11px;
background:rgba(10,16,22,.72);border:1px solid #223140;color:#9fb4c8}
</style>
</head>
<body>
<div class="bar">
<span>视口 <b>4</b> 个</span>
<span class="sp"></span>
<span>拖拽旋转左上透视视图,三个正交视图同步更新</span>
</div>
<div id="stage">
<div id="labels">
<span style="left:10px;top:10px">透视 · 自由旋转</span>
<span style="left:50%;top:10px;margin-left:10px">俯视 · 正交</span>
<span style="left:10px;bottom:10px">正视 · 正交</span>
<span style="left:50%;bottom:10px;margin-left:10px">侧视 · 正交</span>
</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 { OrbitControls } from 'three/addons/controls/OrbitControls.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;
renderer.setScissorTest(true);
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1016);
scene.add(new THREE.HemisphereLight(0xb6d0ea, 0x101820, 1.4));
const sun = new THREE.DirectionalLight(0xffeccd, 2.1);
sun.position.set(-18, 26, 12);
scene.add(sun);
const W = stage.clientWidth;
const H = stage.clientHeight;
const ASPECT = W / H;
const persp = new THREE.PerspectiveCamera(45, ASPECT / 2, 0.1, 300);
persp.position.set(18, 15, 20);
const controls = new OrbitControls(persp, renderer.domElement);
controls.target.set(0, 2, 0);
controls.enableDamping = true;
controls.update();
/* 三个正交相机:俯、正、侧,视野大小按场景尺寸给定 */
const FRUSTUM = 11;
function makeOrtho(dir) {
const cam = new THREE.OrthographicCamera(-FRUSTUM, FRUSTUM, FRUSTUM, -FRUSTUM, 0.1, 200);
cam.position.copy(dir.clone().multiplyScalar(40));
cam.lookAt(0, 2, 0);
cam.updateProjectionMatrix();
return cam;
}
const top = makeOrtho(new THREE.Vector3(0.001, 1, 0.001));
const front = makeOrtho(new THREE.Vector3(0, 0.15, 1));
const side = makeOrtho(new THREE.Vector3(1, 0.15, 0));
/* 场景内容:一片地形加几栋建筑,正交视图里能直接量出尺寸 */
function heightAt(x, z) {
return 0.9 * Math.sin(x * 0.4) * Math.cos(z * 0.33) + 0.4 * Math.sin(x * 0.9 + z * 0.5);
}
const tGeo = new THREE.PlaneGeometry(22, 22, 60, 60);
tGeo.rotateX(-Math.PI / 2);
const tp = tGeo.attributes.position;
for (let i = 0; i < tp.count; i++) {
tp.setY(i, heightAt(tp.getX(i), tp.getZ(i)));
}
tGeo.computeVertexNormals();
scene.add(new THREE.Mesh(
tGeo,
new THREE.MeshStandardMaterial({ color: 0x3d5a63, roughness: 0.9, flatShading: true })
));
const colors = [0xd98e5a, 0x8fb6c9, 0xc9b48f, 0x9a8fae];
for (let i = 0; i < 9; i++) {
const gx = (i % 3 - 1) * 6;
const gz = (((i / 3) | 0) - 1) * 6;
const h = 2 + (i % 4) * 1.4;
const b = new THREE.Mesh(
new THREE.BoxGeometry(2.4, h, 2.4),
new THREE.MeshStandardMaterial({ color: colors[i % 4], roughness: 0.6 })
);
b.position.set(gx, heightAt(gx, gz) + h / 2, gz);
scene.add(b);
}
const axes = new THREE.AxesHelper(6);
scene.add(axes);
const cameras = [
{ cam: persp, x: 0, y: 0.5, w: 0.5, h: 0.5 },
{ cam: top, x: 0.5, y: 0.5, w: 0.5, h: 0.5 },
{ cam: front, x: 0, y: 0, w: 0.5, h: 0.5 },
{ cam: side, x: 0.5, y: 0, w: 0.5, h: 0.5 }
];
function layout() {
const w = stage.clientWidth;
const h = stage.clientHeight;
persp.aspect = (w * 0.5) / (h * 0.5);
persp.updateProjectionMatrix();
cameras.forEach(function (v) {
const vw = Math.floor(w * v.w);
const vh = Math.floor(h * v.h);
if (v.cam.isOrthographicCamera) {
const a = vw / vh;
v.cam.left = -FRUSTUM * a;
v.cam.right = FRUSTUM * a;
v.cam.top = FRUSTUM;
v.cam.bottom = -FRUSTUM;
}
v.cam.aspect = vw / vh;
v.cam.updateProjectionMatrix();
});
}
renderer.setAnimationLoop(function () {
controls.update();
const w = stage.clientWidth;
const h = stage.clientHeight;
cameras.forEach(function (v) {
const vw = Math.floor(w * v.w);
const vh = Math.floor(h * v.h);
const x = Math.floor(w * v.x);
const y = Math.floor(h * v.y);
renderer.setViewport(x, y, vw, vh);
renderer.setScissor(x, y, vw, vh);
renderer.render(scene, v.cam);
});
});
layout();
window.addEventListener('resize', function () {
const w = stage.clientWidth;
const h = stage.clientHeight;
renderer.setSize(w, h);
layout();
});
</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.