bd-admin
Version:
一款能根据需求快速配置vue后台管理的脚手架
493 lines (477 loc) • 18.1 kB
text/typescript
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// "three": "^0.160.0",
// 渲染组合器
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
// 渲染通道
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
// ShaderPass功能:使用后处理Shader创建后处理通道
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
// 发光描边
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
// SMAA抗锯齿通道
import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js";
// 伽马校正
import { GammaCorrectionShader } from "three/examples/jsm/shaders/GammaCorrectionShader.js";
import { Params, Vector3, FnRenderType } from "./interface/index";
import { CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js";
import { CSS3DRenderer } from "three/addons/renderers/CSS3DRenderer.js";
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
import { MoveTo } from "./utils/index";
class Raycaster {
protected readonly event: any;
protected readonly camera: THREE.PerspectiveCamera;
constructor(event: any, camera: THREE.PerspectiveCamera) {
this.event = event;
this.camera = camera;
}
init() {
const raycaster = new THREE.Raycaster(
this.camera.position,
this.normalize(),
);
raycaster.camera = this.camera;
return raycaster;
}
protected normalize = () => {
const vector = new THREE.Vector3(
(this.event.offsetX / window.innerWidth) * 2 - 1,
-(this.event.offsetY / window.innerHeight) * 2 + 1,
0.5,
); //三维坐标对象
vector.unproject(this.camera);
return vector.sub(this.camera.position).normalize();
};
}
class Controls {
public readonly controls: any;
constructor(
camera: THREE.PerspectiveCamera,
domElement: HTMLCanvasElement,
params: Params,
) {
const controls = new OrbitControls(camera, domElement);
const {
enableZoom = true,
zoomSpeed = 0.5,
enableDamping = true,
maxDistance = 1000,
minDistance = 30,
rotateSpeed = 0.5,
maxPolarAngle = Math.PI / 2,
maxAzimuthAngle = Math.PI / 4,
minAzimuthAngle = -Math.PI / 4,
} = params.controls || {};
controls.enableZoom = enableZoom; // 启用或禁用摄像机的缩放。
controls.zoomSpeed = zoomSpeed; // 摄像机缩放的速度
controls.enableDamping = enableDamping; //启用阻尼
controls.maxDistance = maxDistance ? maxDistance : params.far; // 相机向外移动多少
controls.minDistance = minDistance; // 相机向内移动多少
controls.rotateSpeed = rotateSpeed; //旋转的速度
controls.maxPolarAngle = maxPolarAngle; //垂直旋转的角度的上限,范围是0到Math.PI
controls.maxAzimuthAngle = maxAzimuthAngle; //水平旋转的角度的上限,范围是-Math.PI到Math.PI(或Infinity无限制)
controls.minAzimuthAngle = minAzimuthAngle; //水平旋转的角度的下限,范围是-Math.PI到Math.PI(或-Infinity无限制),
controls.screenSpacePanning = false;
this.controls = controls;
}
}
export default class Thre3d {
protected readonly params: Params;
public scene: THREE.Scene; // 网格
public camera: THREE.PerspectiveCamera | null; // 相机
public renderer: THREE.WebGLRenderer | null; // WebGL渲染器
protected directionalLight: THREE.DirectionalLight | null; // 模拟太阳光
private fnEvent: Map<FnRenderType, Function>; // 事件类型函数
public width: number;
public height: number;
protected controls: any; // 轨道控制器
protected composer: any;
protected dome: HTMLElement | null;
protected cameraPositionsFn: Function | null;
protected cameraLookAtFn: Function | null;
labelRenderer: any;
css3Renderer: any;
constructor(params: Params) {
this.params = params;
this.cameraPositionsFn = null;
this.cameraLookAtFn = null;
this.width = 0;
this.height = 0;
this.labelRenderer = null;
this.css3Renderer = null;
this.dome = null;
this.directionalLight = null;
this.fnEvent = new Map<FnRenderType, Function>();
this.controls = null;
this.composer = null;
this.renderer = null;
this.camera = null;
this.scene = new THREE.Scene();
this.#init();
}
#init() {
this.dome = document.querySelector(`#${this.params.id}`);
if (!this.dome) return;
this.width = this.dome.clientWidth; //宽度
this.height = this.dome.clientHeight; //高度
this.camera = new THREE.PerspectiveCamera(
30,
this.width / this.height,
1,
this.params.far ? this.params.far : 10000,
);
this.renderer = new THREE.WebGLRenderer({
alpha: true, //渲染器透明
antialias: true, //抗锯齿
precision: "highp", //着色器开启高精度
logarithmicDepthBuffer: true, // 是否使用对数深度缓存
});
this.renderer.render(this.scene, this.camera); //执行渲染操作
this.scene.fog = new THREE.Fog(0xffffff, 400, 10000);
this.dome.addEventListener("click", this.#onMouseClick, false); //点击事件
// WebGL渲染器
const renderer = this.renderer;
//开启HiDPI设置
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.outputColorSpace = THREE.SRGBColorSpace;
this.dome.appendChild(renderer.domElement);
const labelRenderer = new CSS2DRenderer();
labelRenderer.domElement.style.position = "absolute";
// 相对标签原位置位置偏移大小
labelRenderer.domElement.style.top = "0px";
labelRenderer.domElement.style.left = "0px";
labelRenderer.domElement.style.pointerEvents = "none";
this.labelRenderer = labelRenderer;
this.dome.appendChild(labelRenderer.domElement);
// 创建一个CSS3渲染器CSS3DRenderer
const css3Renderer = new CSS3DRenderer();
// HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
css3Renderer.domElement.style.position = "absolute";
css3Renderer.domElement.style.top = "0px";
//设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
css3Renderer.domElement.style.pointerEvents = "none";
this.css3Renderer = css3Renderer;
this.dome.appendChild(css3Renderer.domElement);
this.initControls();
this.#reset();
this.#setScene();
}
// 添加发光通道
addEffectComposer = () => {
const effectColorSpaceConversion = new ShaderPass(GammaCorrectionShader);
const composer = new EffectComposer(this.renderer);
const renderPass = new RenderPass(this.scene, this.camera);
composer.addPass(renderPass);
const outlinePass = new OutlinePass(
new THREE.Vector2(this.width, this.height),
this.scene,
this.camera,
);
//模型描边颜色,默认白色
outlinePass.visibleEdgeColor.set(0xffff00);
//高亮发光描边厚度
outlinePass.edgeThickness = 4;
//高亮描边发光强度
outlinePass.edgeStrength = 6;
//模型闪烁频率控制,默认0不闪烁
outlinePass.pulsePeriod = 2;
composer.addPass(outlinePass);
const pixelRatio = this.renderer!.getPixelRatio();
// SMAA抗锯齿通道
const smaaPass = new SMAAPass(
this.width * pixelRatio,
this.height * pixelRatio,
);
composer.addPass(smaaPass);
this.renderer!.domElement.style.touchAction = "none";
this.renderer!.domElement.addEventListener("pointermove", onPointerMove);
const mouse = new THREE.Vector2();
let selectedObjects: THREE.Object3D[] = [];
function onPointerMove(event: any) {
if (event.isPrimary === false) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
checkIntersection();
}
const raycaster = new THREE.Raycaster();
const checkIntersection = () => {
raycaster.setFromCamera(mouse, this.camera!);
const intersects = raycaster.intersectObject(this.scene, true);
if (intersects.length > 0) {
const selectedObject = intersects[0].object;
addSelectedObject(selectedObject);
outlinePass.selectedObjects = selectedObjects;
} else {
// outlinePass.selectedObjects = [];
}
};
function addSelectedObject(object: THREE.Object3D) {
selectedObjects = [];
selectedObjects.push(object);
}
composer.addPass(effectColorSpaceConversion);
this.composer = composer;
};
#setScene = () => {
this.scene.background = new THREE.Color(0x333333);
// this.scene.fog = new THREE.Fog(0x333333, 10, 15);
this.scene.environment = new RGBELoader().load(
"https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr",
);
this.scene.environment.mapping = THREE.EquirectangularReflectionMapping;
// 天空
// 注释,有需要自己添加背景图
// this.scene.background = new THREE.CubeTextureLoader()
// .setPath("/texture/sky/")
// .load([
// "sky.left.jpg",
// "sky.right.jpg",
// "sky.top.jpg",
// "sky.bottom.jpg",
// "sky.back.jpg",
// "sky.front.jpg",
// ]);
// const AxesHelper = new THREE.AxesHelper(10000);
// AxesHelper.name = "辅助坐标系";
};
//点击事件
#onMouseClick = (event: any) => {
event.preventDefault();
const raycaster = new Raycaster(event, this.camera!).init();
const intersects = raycaster.intersectObjects(this.scene.children);
const selected = intersects[0]; //intersects是射线沿着摄像机机镜头的方向穿过的所有物体,这里取第一个物体
if (!selected) return;
const {
object: { type },
} = selected;
const x = Math.floor(selected.point.x * 100) / 100;
const y = Math.floor(selected.point.y * 100) / 100;
const z = Math.floor(selected.point.z * 100) / 100;
console.log("x:" + x + ",y:" + y + ",z:" + z);
let returnFlag = false;
let sFn, mFn;
switch (type) {
case "Sprite":
sFn = this.fnEvent.get("spriteClick");
if (sFn) sFn(selected);
returnFlag = true;
console.log("精灵图", selected);
break;
case "Mesh":
mFn = this.fnEvent.get("modelClick");
if (mFn) mFn(selected);
break;
default:
console.log("暂未定义的类型", type);
}
if (returnFlag) return;
const cFn = this.fnEvent.get("click");
if (cFn) cFn(new THREE.Vector3(x, y, z));
};
#onMouseMove = (event: any) => {
event.preventDefault();
const raycaster = new Raycaster(event, this.camera!).init();
const intersects = raycaster.intersectObjects(this.scene.children);
const selected = intersects[0]; //intersects是射线沿着摄像机机镜头的方向穿过的所有物体,这里取第一个物体
if (!selected) return;
const x = Math.floor(selected.point.x * 1000) / 1000;
const y = Math.floor(selected.point.y * 1000) / 1000;
const z = Math.floor(selected.point.z * 1000) / 1000;
const address = new THREE.Vector3(x, y, z);
const mFn = this.fnEvent.get("mouseMove");
if (mFn) mFn(address);
return address;
};
render = () => {
if (this.renderer) {
this.renderer.render(this.scene, this.camera!); //执行渲染操作
}
if (this.composer) {
this.composer.render(this.scene, this.camera);
}
if (this.labelRenderer) {
this.labelRenderer.render(this.scene, this.camera);
}
if (this.css3Renderer) {
this.css3Renderer.render(this.scene, this.camera);
}
if (this.controls) {
this.controls.update();
}
if (this.cameraPositionsFn) {
this.cameraPositionsFn();
}
if (this.cameraLookAtFn) {
this.cameraLookAtFn();
}
if (
this.directionalLight &&
this.params.shadow &&
this.params.timeMultiply
) {
const r = Date.now() * (0.0001 / this.params.timeMultiply);
this.directionalLight.position.x = 700 * Math.cos(r);
this.directionalLight.position.y = 700 * Math.sin(r);
// const angle: number =
// (new THREE.Vector2(
// this.directionalLight.position.x,
// this.directionalLight.position.y,
// ).angle() *
// 180) /
// Math.PI;
// if (1 < angle && angle < 180) {
// console.log("白天", angle);
// } else {
// console.log("夜晚", angle);
// }
}
};
resize = () => {
this.dome = document.querySelector(`#${this.params.id}`);
if (!this.dome) return;
this.width = this.dome.clientWidth; //宽度
this.height = this.dome.clientHeight; //高度
this.#reset();
};
#reset = () => {
this.camera!.aspect = this.width / this.height;
this.camera!.updateProjectionMatrix();
this.renderer!.setSize(this.width, this.height);
this.labelRenderer.setSize(this.width, this.height);
this.css3Renderer.setSize(this.width, this.height);
};
/** 平行光
* @param {Vector3} position 坐标
* @param {boolean} intensity 光线强度
*/
addSun(position: Vector3 | undefined, intensity: number | undefined = 2.5) {
const { x = 2000, y = 40000, z = 2000 } = position || {};
if (this.params.shadow) {
const directionalLight = new THREE.DirectionalLight(0xffffff, intensity); // 新建一个平行光源,颜色未白色,强度为1
directionalLight.position.set(x, y, z); // 将此平行光源调整到一个合适的位置
directionalLight.castShadow = true; // 将此平行光源产生阴影的属性打开
const d = 100; //阴影范围 // 设置平行光的的阴影属性,即一个长方体的长宽高,在设定值的范围内的物体才会产生阴影
directionalLight.shadow.camera.left = -d;
directionalLight.shadow.camera.right = d;
directionalLight.shadow.camera.top = d;
directionalLight.shadow.camera.bottom = -d;
directionalLight.shadow.camera.near = 20;
directionalLight.shadow.camera.far = 8000;
directionalLight.shadow.mapSize.x = 2048; // 定义阴影贴图的宽度和高度,必须为2的整数此幂
directionalLight.shadow.mapSize.y = 2048; // 较高的值会以计算时间为代价提供更好的阴影质量
directionalLight.shadow.bias = -0.0005; //解决条纹阴影的出现
this.directionalLight = directionalLight;
directionalLight.name = "sunShadow";
this.scene.add(directionalLight); // 将此平行光源加入场景中,我们才可以看到这个光源
} else {
const group = new THREE.Group();
group.name = "sun";
const directionalLight1 = new THREE.DirectionalLight(
0xffffff,
intensity / 4,
);
directionalLight1.position.set(x, y, z);
group.add(directionalLight1);
this.scene.add(group);
}
}
// 注册事件
on(type: FnRenderType, fn: Function) {
this.fnEvent.set(type, fn);
}
// 移除事件
removeOn(type: FnRenderType) {
const m = this.fnEvent.get(type);
if (m) this.fnEvent.delete(type);
}
/**
* 切换视角
* @param {Vector3} position position目的坐标
* @param {boolean} animation 是否启动动画
* @param {Function} callback 到达目的回调函数
*/
setCameraPositions = (
position: Vector3,
animation?: boolean,
callback?: Function,
) => {
if (!animation) {
this.cameraPositionsFn = null;
this.camera!.position.set(position.x, position.y, position.z);
if (callback) callback();
return;
}
const endPosition = new THREE.Vector3(position.x, position.y, position.z); // 结束位置
const M = new MoveTo(this.camera!.position, endPosition, 5);
const target = JSON.parse(JSON.stringify(this.controls.target));
this.cameraPositionsFn = () => {
this.controls.target.set(0, 0, 0);
this.controls.update();
const manhattanDistanceTo = M.move();
this.controls.target.set(target.x, target.y, target.z);
this.controls.update();
if (manhattanDistanceTo < 1) {
this.cameraPositionsFn = null;
}
//
};
};
/**切换注视点
* @param {Vector3} position position目的坐标
* @param {boolean} animation 是否启动动画
* @param {Function} callback 到达目的回调函数
* @returns
*/
setCameraLookAt = (
position: Vector3,
animation?: boolean,
callback?: Function,
) => {
if (!animation) {
if (!this.camera) return;
this.cameraLookAtFn = null;
this.camera.lookAt(position.x, position.y, position.z);
if (!this.controls) return;
this.controls.target.set(position.x, position.y, position.z);
if (callback) callback();
return;
}
const endPosition = new THREE.Vector3(position.x, position.y, position.z); // 结束位置
const M = new MoveTo(this.controls.target, endPosition, 30);
this.cameraLookAtFn = () => {
const manhattanDistanceTo = M.move();
if (manhattanDistanceTo < 1) {
this.controls.target.copy(endPosition);
this.cameraLookAtFn = null;
if (callback) callback();
}
};
};
removeControls() {
if (!this.controls) return;
this.controls.object = null;
this.controls = null;
}
initControls() {
if (!this.camera || !this.renderer) return;
const {
cameraPosition: { x = 0, y = 100, z = 200 },
lookAt,
} = this.params;
const { x: lookAtX = 0, y: lookAtY = 0, z: lookAtZ = 0 } = lookAt || {};
// 设置相机控件轨道控制器OrbitControls
const controls = new Controls(
this.camera,
this.renderer.domElement,
this.params,
).controls;
controls.target.set(lookAtX, lookAtY, lookAtZ);
this.controls = controls;
this.camera.lookAt(lookAtX, lookAtY, lookAtZ);
this.camera.position.set(x, y, z);
this.controls.update();
}
}