🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
前置代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
html,
body {
margin: 0;
height: 100%;
}
#c {
width: 100%;
height: 100%;
display: block;
}
.split {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
}
.split>div {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="c">
</canvas>
<div class="split">
<div id="view1"></div>
<div id="view2"></div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three@0.174.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.174.0/examples/jsm/"
}
}
</script>
<script type="module" src="./index.js"></script>
</body>
</html>
透視攝像機PerspectiveCamera
PerspectiveCamera 通過四個屬性來定義一個視錐, near定義視錐前端, far定義遠端, fov是視野, 通過計算正確的高度來從攝像機的位置獲取指定的以near為單位的視野, 定義的是視錐的前端和遠端的高度 aspect間接地定義了視錐前端和遠端的寬度, 實際上視錐的寬度是通過高度乘以 aspect 來得到的
下面這個例子我們使用 three 的剪函數, 把視圖分成兩部分, 主視圖正常渲染, 輔視圖用來觀察 cameraHelper 的渲染
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
function main() {
const canvas = document.querySelector("#c");
const view1Elem = document.querySelector("#view1");
const view2Elem = document.querySelector("#view2");
const renderer = new THREE.WebGLRenderer({ antialias: true, canvas });
// #region 左視圖的相機
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const cameraHelper = new THREE.CameraHelper(camera);
const controls = new OrbitControls(camera, view1Elem);
controls.target.set(0, 5, 0);
controls.update();
// #endregion
// #region 右視圖的相機
const camera2 = new THREE.PerspectiveCamera(
60, // fov
2, // aspect
0.1, // near
500, // far
);
camera2.position.set(40, 10, 30);
camera2.lookAt(0, 5, 0);
const controls2 = new OrbitControls(camera2, view2Elem);
controls2.target.set(0, 5, 0);
controls2.update();
// #endregion
/**
* 設置裁剪區域和視口, 返回寬高比
* @param {HTMLElement} elem
* @returns
*/
function setScissorForElement(elem) {
// 獲取 canvas 與元素的邊界矩形
const canvasRect = canvas.getBoundingClientRect();
const elemRect = elem.getBoundingClientRect();
// 相對位置計算元素在 canvas 內的左右上下邊界
const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
const left = Math.max(0, elemRect.left - canvasRect.left);
const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
const top = Math.max(0, elemRect.top - canvasRect.top);
const width = Math.min(canvasRect.width, right - left);
const height = Math.min(canvasRect.height, bottom - top);
// 設置裁剪
const positiveYUpBottom = canvasRect.height - bottom;
// 對 renderer 設置裁剪區域和視口
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
return width / height;
}
// gui 使用,限制對象中屬性的最大值最小值
class MinMaxGUIHelper {
constructor(obj, minProp, maxProp, minDif) {
this.obj = obj;
this.minProp = minProp;
this.maxProp = maxProp;
this.minDif = minDif;
}
get min() {
return this.obj[this.minProp];
}
set min(v) {
this.obj[this.minProp] = v;
this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
}
get max() {
return this.obj[this.maxProp];
}
set max(v) {
this.obj[this.maxProp] = v;
this.min = this.min; // this will call the min setter
}
}
// #region 添加相機屬性的gui界面
const gui = new GUI();
gui.add(camera, "fov", 1, 180);
const minMaxGUIHelper = new MinMaxGUIHelper(camera, "near", "far", 0.1);
gui.add(minMaxGUIHelper, "min", 0.1, 50, 0.1).name("near");
gui.add(minMaxGUIHelper, "max", 0.1, 50, 0.1).name("far");
// #endregion
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
scene.add(cameraHelper);
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("https://threejs.org/manual/examples/resources/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
//texture.colorSpace = THREE.SRGBColorSpace;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8AC" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions,
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
{
const color = 0xffffff;
const intensity = 3;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
resizeRendererToDisplaySize(renderer);
// 啓用剪刀函數
renderer.setScissorTest(true);
// #region 視圖1 渲染
const aspect1 = setScissorForElement(view1Elem);
camera.aspect = aspect1;
camera.updateProjectionMatrix();
// 不在視圖 1中渲染 helper
cameraHelper.visible = false;
cameraHelper.update();
renderer.render(scene, camera);
// #endregion
// #region 視圖2 渲染
const aspect2 = setScissorForElement(view2Elem);
camera2.aspect = aspect2;
camera2.updateProjectionMatrix();
// 在第二台攝像機中繪製cameraHelper
cameraHelper.visible = true;
// 單獨給視圖 2 設置個背景色
scene.background.set(0x000040);
renderer.render(scene, camera2);
// #endregion
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
正交攝像機OrthographicCamera
與透視攝像機不同的是, 它需要設置left right top bottom near 和 far 指定一個長方形, 使得視野是平行的而不是透視的
使用 zoom 屬性可以縮放世界 -> 屏幕的映射比例, 不改變實際尺寸
< 1 看到更多 > 1 看到更少
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
function main() {
const canvas = document.querySelector("#c");
const view1Elem = document.querySelector("#view1");
const view2Elem = document.querySelector("#view2");
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas,
logarithmicDepthBuffer: true,
});
// #region 左視圖的相機
const size = 1;
const near = 5;
const far = 50;
const camera = new THREE.OrthographicCamera(-size, size, size, -size, near, far);
camera.zoom = 0.2;
camera.position.set(0, 20, 0);
// camera.lookAt(0, 0, 0);
const cameraHelper = new THREE.CameraHelper(camera);
const controls = new OrbitControls(camera, view1Elem);
controls.target.set(2, 0, 0);
controls.update();
// #endregion
// #region 右視圖的相機
const camera2 = new THREE.PerspectiveCamera(
60, // fov
2, // aspect
0.1, // near
500, // far
);
camera2.position.set(40, 10, 30);
camera2.lookAt(0, 10, 0);
const controls2 = new OrbitControls(camera2, view2Elem);
controls2.target.set(0, 5, 0);
controls2.update();
// #endregion
/**
* 設置裁剪區域和視口, 返回寬高比
* @param {HTMLElement} elem
* @returns
*/
function setScissorForElement(elem) {
// 獲取 canvas 與元素的邊界矩形
const canvasRect = canvas.getBoundingClientRect();
const elemRect = elem.getBoundingClientRect();
// 相對位置計算元素在 canvas 內的左右上下邊界
const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
const left = Math.max(0, elemRect.left - canvasRect.left);
const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
const top = Math.max(0, elemRect.top - canvasRect.top);
const width = Math.min(canvasRect.width, right - left);
const height = Math.min(canvasRect.height, bottom - top);
// 設置裁剪
const positiveYUpBottom = canvasRect.height - bottom;
// 對 renderer 設置裁剪區域和視口
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
return width / height;
}
// gui 使用,限制對象中屬性的最大值最小值
class MinMaxGUIHelper {
constructor(obj, minProp, maxProp, minDif) {
this.obj = obj;
this.minProp = minProp;
this.maxProp = maxProp;
this.minDif = minDif;
}
get min() {
return this.obj[this.minProp];
}
set min(v) {
this.obj[this.minProp] = v;
this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
}
get max() {
return this.obj[this.maxProp];
}
set max(v) {
this.obj[this.maxProp] = v;
this.min = this.min; // this will call the min setter
}
}
// #region 添加相機屬性的gui界面
const gui = new GUI();
// gui.add(camera, "fov", 1, 180);
const minMaxGUIHelper = new MinMaxGUIHelper(camera, "near", "far", 0.1);
gui.add(minMaxGUIHelper, "min", 0.1, 50, 0.1).name("near");
gui.add(minMaxGUIHelper, "max", 0.1, 50, 0.1).name("far");
gui.add(camera, "zoom", 0.01, 1).name("zoom").listen(); // 調整相機展現多少單位大小
// #endregion
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
scene.add(cameraHelper);
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("https://threejs.org/manual/examples/resources/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
//texture.colorSpace = THREE.SRGBColorSpace;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8AC" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions,
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
{
const color = 0xffffff;
const intensity = 3;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
resizeRendererToDisplaySize(renderer);
// 啓用剪刀函數
renderer.setScissorTest(true);
// #region 視圖1 渲染
const aspect1 = setScissorForElement(view1Elem);
camera.left = -aspect1;
camera.right = aspect1;
camera.updateProjectionMatrix();
// 不在視圖 1中渲染 helper
cameraHelper.visible = false;
cameraHelper.update();
renderer.render(scene, camera);
// #endregion
// #region 視圖2 渲染
const aspect2 = setScissorForElement(view2Elem);
camera2.aspect = aspect2;
camera2.updateProjectionMatrix();
// 在第二台攝像機中繪製cameraHelper
cameraHelper.visible = true;
// 單獨給視圖 2 設置個背景色
scene.background.set(0x000040);
renderer.render(scene, camera2);
// #endregion
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();