From 3f64da09039dcd8026253ff0cbfbc052a1a68025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=8D=E5=87=AF=E9=94=8B?= Date: Sat, 1 Jun 2024 17:33:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:gltf=E4=B8=89=E7=BB=B4=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BF=A1=E6=81=AF=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../packages/ThreeGltf/ThreeGltf.vue | 194 +++++++++----- .../packages/ThreeLayer/CSS2DRenderer.js | 182 +++++++++++++ .../packages/ThreeLayer/CustomThreeLayer.ts | 222 +++++++++------- .../packages/ThreeLayer/ThreeLayer.vue | 165 ++++++------ test/views/three/Gltf.vue | 247 ++++++++++-------- 5 files changed, 660 insertions(+), 350 deletions(-) create mode 100644 src/vue-amap-extra/packages/ThreeLayer/CSS2DRenderer.js diff --git a/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue b/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue index b416bbd..16c76af 100644 --- a/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue +++ b/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue @@ -1,84 +1,130 @@ diff --git a/src/vue-amap-extra/packages/ThreeLayer/CSS2DRenderer.js b/src/vue-amap-extra/packages/ThreeLayer/CSS2DRenderer.js new file mode 100644 index 0000000..4db6def --- /dev/null +++ b/src/vue-amap-extra/packages/ThreeLayer/CSS2DRenderer.js @@ -0,0 +1,182 @@ +import { Matrix4, Object3D, Vector2, Vector3 } from 'three'; + +class CSS2DObject extends Object3D { + constructor(element = document.createElement('div')) { + super(); + + this.isCSS2DObject = true; + + this.element = element; + + this.element.style.position = 'absolute'; + this.element.style.userSelect = 'none'; + + this.element.setAttribute('draggable', false); + + this.center = new Vector2(0.5, 0.5); // ( 0, 0 ) is the lower left; ( 1, 1 ) is the top right + + this.addEventListener('removed', function () { + this.traverse(function (object) { + if (object.element instanceof Element && object.element.parentNode !== null) { + object.element.parentNode.removeChild(object.element); + } + }); + }); + } + + copy(source, recursive) { + super.copy(source, recursive); + + const self = this; + + self.element = source.element.cloneNode(true); + + self.center = source.center; + + return self; + } +} + +// + +const _vector = new Vector3(); +const _viewMatrix = new Matrix4(); +const _viewProjectionMatrix = new Matrix4(); +const _a = new Vector3(); +const _b = new Vector3(); + +class CSS2DRenderer { + constructor(parameters = {}) { + const _this = this; + + let _width, _height; + let _widthHalf, _heightHalf; + + const cache = { + objects: new WeakMap(), + }; + + const domElement = + parameters.element !== undefined ? parameters.element : document.createElement('div'); + + domElement.style.overflow = 'hidden'; + + this.domElement = domElement; + + this.getSize = function () { + return { + width: _width, + height: _height, + }; + }; + + this.render = function (scene, camera) { + if (scene.matrixWorldAutoUpdate === true) scene.updateMatrixWorld(); + if (camera.parent === null && camera.matrixWorldAutoUpdate === true) + camera.updateMatrixWorld(); + + _viewMatrix.copy(camera.matrixWorldInverse); + _viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix); + + renderObject(scene, scene, camera); + zOrder(scene); + }; + + this.setSize = function (width, height) { + _width = width; + _height = height; + + _widthHalf = _width / 2; + _heightHalf = _height / 2; + + domElement.style.width = width + 'px'; + domElement.style.height = height + 'px'; + }; + + function renderObject(object, scene, camera) { + if (object.isCSS2DObject) { + _vector.setFromMatrixPosition(object.matrixWorld); + _vector.applyMatrix4(_viewProjectionMatrix); + + const visible = + object.visible === true && + _vector.z >= -1 && + _vector.z <= 1 && + object.layers.test(camera.layers) === true; + object.element.style.display = visible === true ? '' : 'none'; + + if (visible === true) { + object.onBeforeRender(_this, scene, camera); + + const element = object.element; + + element.style.transform = + 'translate(' + + -100 * object.center.x + + '%,' + + -100 * object.center.y + + '%)' + + 'translate(' + + (_vector.x * _widthHalf + _widthHalf) + + 'px,' + + (-_vector.y * _heightHalf + _heightHalf) + + 'px)'; + + if (element.parentNode !== domElement) { + domElement.appendChild(element); + } + + object.onAfterRender(_this, scene, camera); + } + + const objectData = { + distanceToCameraSquared: getDistanceToSquared(camera, object), + }; + + cache.objects.set(object, objectData); + } + + for (let i = 0, l = object.children.length; i < l; i++) { + renderObject(object.children[i], scene, camera); + } + } + + function getDistanceToSquared(object1, object2) { + _a.setFromMatrixPosition(object1.matrixWorld); + _b.setFromMatrixPosition(object2.matrixWorld); + + return _a.distanceToSquared(_b); + } + + function filterAndFlatten(scene) { + const result = []; + + scene.traverse(function (object) { + if (object.isCSS2DObject) result.push(object); + }); + + return result; + } + + function zOrder(scene) { + const sorted = filterAndFlatten(scene).sort(function (a, b) { + if (a.renderOrder !== b.renderOrder) { + return b.renderOrder - a.renderOrder; + } + + const distanceA = cache.objects.get(a).distanceToCameraSquared; + const distanceB = cache.objects.get(b).distanceToCameraSquared; + + return distanceA - distanceB; + }); + + const zMax = sorted.length; + + for (let i = 0, l = sorted.length; i < l; i++) { + sorted[i].element.style.zIndex = zMax - i; + } + } + } +} + +export { CSS2DObject, CSS2DRenderer }; diff --git a/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts b/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts index dbd71c6..ca87abc 100644 --- a/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts +++ b/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts @@ -11,102 +11,113 @@ import { Vector2, AxesHelper, Raycaster, - Clock -} from 'three'; -import {merge, bind} from "lodash-es"; -import {HDRCubeTextureLoader} from "three/examples/jsm/loaders/HDRCubeTextureLoader.js"; -import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; -import {ThreeLayer} from '@vuemap/three-layer' -import type { Texture, Camera, - WebGLRenderer, - Scene, Object3D -} from 'three'; -import type {HDROptions, LightOption} from "./Type"; -import type {ThreeLayerOptions} from '@vuemap/three-layer' + Clock, +} from "three"; +import { merge, bind } from "lodash-es"; +import { HDRCubeTextureLoader } from "three/examples/jsm/loaders/HDRCubeTextureLoader.js"; +import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; +import { ThreeLayer } from "@vuemap/three-layer"; +import type { Texture, Camera, WebGLRenderer, Scene, Object3D } from "three"; +import type { HDROptions, LightOption } from "./Type"; +import type { ThreeLayerOptions } from "@vuemap/three-layer"; +import { CSS2DRenderer } from "./CSS2DRenderer"; -interface Options extends ThreeLayerOptions{ - lights?: LightOption[] // 灯光数组 - hdr?: HDROptions // 开启HDR配置 - axesHelper: boolean // 是否开启箭头,用于debug,默认不开启 +interface Options extends ThreeLayerOptions { + lights?: LightOption[]; // 灯光数组 + hdr?: HDROptions; // 开启HDR配置 + axesHelper: boolean; // 是否开启箭头,用于debug,默认不开启 + createCssRender?: boolean; //是否创建CssRender } -class CustomThreeLayer extends ThreeLayer{ - +class CustomThreeLayer extends ThreeLayer { lightTypes = { AmbientLight, // 环境光 环境光会均匀的照亮场景中的所有物体 DirectionalLight, // 平行光 平行光是沿着特定方向发射的光 HemisphereLight, // 半球光 光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。 PointLight, // 点光源 从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光 RectAreaLight, // 平面光光源 平面光光源从一个矩形平面上均匀地发射光线。这种光源可以用来模拟像明亮的窗户或者条状灯光光源 - SpotLight // 聚光灯 光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大 - } - raycaster: Raycaster | undefined // 射线,用于判断点击或者鼠标移动是否碰到物体 + SpotLight, // 聚光灯 光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大 + }; + raycaster: Raycaster | undefined; // 射线,用于判断点击或者鼠标移动是否碰到物体 mouse: Vector2; envMap: any; // HDR的环境贴图 clickFun: any; hoverFun: any; resizeFun: any; effectComposer?: EffectComposer; - renderPass: any - passNum = 0 - passList = [] as any[] - clock = new Clock() + renderPass: any; + passNum = 0; + passList = [] as any[]; + clock = new Clock(); preHoverGroup = null as Object3D | null; + cssRenderer = null as CSS2DRenderer | null; constructor(map: any, options: Options, callback: () => void) { - options.onInit = (render,scene) => { + options.onInit = (render, scene) => { this.raycaster = new Raycaster(); - if(options.axesHelper){ - const axesHelper = new AxesHelper( 10000 ); - scene.add( axesHelper ); + if (options.axesHelper) { + const axesHelper = new AxesHelper(10000); + scene.add(axesHelper); } this.renderer?.setPixelRatio(window.devicePixelRatio); + if (options.createCssRender) this.creatCssRender(map); this.createEffect(); this.createLights(options.lights || []); this.createHDR(options.hdr); this.bindEvents(); - if(callback){ - callback() + if (callback) { + callback(); } - } + }; options.onRender = (render, scene, camera) => { - if(this.passNum > 0){ - this.effectComposer?.render(this.clock.getDelta()) - }else{ + if (this.passNum > 0) { + this.effectComposer?.render(this.clock.getDelta()); + } else { this.renderer?.render(this.scene as Scene, camera as Camera); + this.cssRenderer?.render(this.scene as Scene, camera as Camera); } - } - super(map, options) + }; + super(map, options); this.mouse = new Vector2(); } updateEffectComposerSize() { - if(this.effectComposer && this.renderer){ - const size = this.renderer.getSize( new Vector2() ); - this.effectComposer.setSize( size.x , size.y); + if (this.effectComposer && this.renderer) { + const size = this.renderer.getSize(new Vector2()); + this.effectComposer.setSize(size.x, size.y); + this.cssRenderer?.setSize(size.x, size.y); } } createEffect() { - const size = this.renderer?.getSize( new Vector2() ); - this.effectComposer = new EffectComposer( this.renderer as WebGLRenderer ); - this.effectComposer.setSize( size?.x as number, size?.y as number); - + const size = this.renderer?.getSize(new Vector2()); + this.effectComposer = new EffectComposer(this.renderer as WebGLRenderer); + this.effectComposer.setSize(size?.x as number, size?.y as number); + this.cssRenderer?.setSize(size?.x as number, size?.y as number); // const renderPass = new ThreeRenderPass( this.scene, this.camera ); // this.renderPass = renderPass; // this.effectComposer.addPass(renderPass); } - addPass(pass: any){ + creatCssRender(map: any) { + const element = map.getContainer().querySelector(".amap-markers"); + this.cssRenderer = new CSS2DRenderer(); + this.cssRenderer.domElement.style.position = "absolute"; + this.cssRenderer.domElement.style.left = "0"; + this.cssRenderer.domElement.style.top = "0"; + element.appendChild(this.cssRenderer.domElement); + } + + addPass(pass: any) { this.effectComposer?.addPass(pass); this.passNum++; this.passList.push(pass); } - removePass(pass: any){ - const index = this.passList.indexOf( pass ); - if ( index !== - 1 ) { - this.passList.splice( index, 1 ); + removePass(pass: any) { + const index = this.passList.indexOf(pass); + if (index !== -1) { + this.passList.splice(index, 1); } this.effectComposer?.removePass(pass); this.passNum--; @@ -114,14 +125,20 @@ class CustomThreeLayer extends ThreeLayer{ createLights(lights: LightOption[] | undefined) { const defaultLightOptions = { - type: 'DirectionalLight', // 灯光类型, 可选值见下面的字典 - args: [] // 灯光初始化时需要的参数,具体参数顺序可以查看threejs官网灯光的说明。 采用 ...args 的方式进行初始化 + type: "DirectionalLight", // 灯光类型, 可选值见下面的字典 + args: [], // 灯光初始化时需要的参数,具体参数顺序可以查看threejs官网灯光的说明。 采用 ...args 的方式进行初始化 }; if (lights && lights.length > 0) { - lights.forEach(lightOptions => { - lightOptions = merge({}, defaultLightOptions, lightOptions) as LightOption; + lights.forEach((lightOptions) => { + lightOptions = merge( + {}, + defaultLightOptions, + lightOptions + ) as LightOption; if (this.lightTypes[lightOptions.type]) { - const light = new this.lightTypes[lightOptions.type](...lightOptions.args); + const light = new this.lightTypes[lightOptions.type]( + ...lightOptions.args + ); const position = lightOptions.position; const lookAt = lightOptions.lookAt; if (position) { @@ -132,7 +149,7 @@ class CustomThreeLayer extends ThreeLayer{ } this.add(light); } else { - console.warn('当前设置的灯光类型不存在'); + console.warn("当前设置的灯光类型不存在"); } }); } @@ -142,12 +159,16 @@ class CustomThreeLayer extends ThreeLayer{ if (!hdr) { return; } - const options = merge({}, { - urls: [], // HDR贴图下载地址,需要6个文件,代表6个方向 - path: '/', // HDR下载地址的路径前缀 - // roughness: 0.0, - exposure: 1.0 // 光亮程度 - }, hdr); + const options = merge( + {}, + { + urls: [], // HDR贴图下载地址,需要6个文件,代表6个方向 + path: "/", // HDR下载地址的路径前缀 + // roughness: 0.0, + exposure: 1.0, // 光亮程度 + }, + hdr + ); const render = this.renderer as WebGLRenderer; render.physicallyCorrectLights = true; render.outputEncoding = sRGBEncoding; @@ -191,24 +212,24 @@ class CustomThreeLayer extends ThreeLayer{ this.clickFun = bind(this._clickEvent, this); this.hoverFun = bind(this._hoverEvent, this); this.resizeFun = bind(this.updateEffectComposerSize, this); - if(this.canvas){ - this.canvas.addEventListener('click', this.clickFun, false); - this.canvas.addEventListener('mousemove', this.hoverFun, false); - }else{ - this.map.on('click', this.clickFun); - this.map.on('mousemove', this.hoverFun); + if (this.canvas) { + this.canvas.addEventListener("click", this.clickFun, false); + this.canvas.addEventListener("mousemove", this.hoverFun, false); + } else { + this.map.on("click", this.clickFun); + this.map.on("mousemove", this.hoverFun); } - this.map.on('resize', this.resizeFun); + this.map.on("resize", this.resizeFun); } ubBindEvents() { - this.map.off('click', this.clickFun); - this.map.off('mousemove', this.hoverFun); - this.map.on('off', this.resizeFun); + this.map.off("click", this.clickFun); + this.map.off("mousemove", this.hoverFun); + this.map.on("off", this.resizeFun); } _getOriginEvent(e: any) { - if(e.originEvent){ + if (e.originEvent) { return e.originEvent; } return e; @@ -218,10 +239,10 @@ class CustomThreeLayer extends ThreeLayer{ e = this._getOriginEvent(e); const group = this._intersectGltf(e); if (group) { - if(group.userData.$vue){ - group.userData.$vue.$emit('click', group); + if (group.userData.$vue) { + group.userData.$vue.$emit("click", group); } - this.emit('click', group); + this.emit("click", group); } } @@ -231,19 +252,22 @@ class CustomThreeLayer extends ThreeLayer{ if (group) { if (!group.userData.isHover) { group.userData.isHover = true; - if(group.userData.$vue){ - group.userData.$vue.$emit('mouseover', group); - }else{ - this.emit('mouseover', group); + if (group.userData.$vue) { + group.userData.$vue.$emit("mouseover", group); + } else { + this.emit("mouseover", group); } } } - if(this.preHoverGroup){ - if(!group || this.preHoverGroup.uuid !== group.uuid){ - if(this.preHoverGroup.userData.$vue){ - this.preHoverGroup.userData.$vue.$emit('mouseout', this.preHoverGroup); - }else{ - this.emit('mouseout', this.preHoverGroup); + if (this.preHoverGroup) { + if (!group || this.preHoverGroup.uuid !== group.uuid) { + if (this.preHoverGroup.userData.$vue) { + this.preHoverGroup.userData.$vue.$emit( + "mouseout", + this.preHoverGroup + ); + } else { + this.emit("mouseout", this.preHoverGroup); } this.preHoverGroup.userData.isHover = false; } @@ -258,13 +282,24 @@ class CustomThreeLayer extends ThreeLayer{ // window.pageYOffset 鼠标滚动的距离 // clientTop 一个元素顶部边框的宽度 - const offsetTop = getBoundingClientRect.top + window.pageYOffset - client.clientTop; - const offsetLeft = getBoundingClientRect.left + window.pageXOffset - client.clientLeft; - this.mouse.x = ((e.x + window.pageXOffset - offsetLeft) / getBoundingClientRect.width) * 2 - 1; - this.mouse.y = -((e.y + window.pageYOffset - offsetTop) / getBoundingClientRect.height) * 2 + 1; + const offsetTop = + getBoundingClientRect.top + window.pageYOffset - client.clientTop; + const offsetLeft = + getBoundingClientRect.left + window.pageXOffset - client.clientLeft; + this.mouse.x = + ((e.x + window.pageXOffset - offsetLeft) / getBoundingClientRect.width) * + 2 - + 1; + this.mouse.y = + -((e.y + window.pageYOffset - offsetTop) / getBoundingClientRect.height) * + 2 + + 1; const camera = this.camera; this.raycaster?.setFromCamera(this.mouse, camera as Camera); - const intersects = this.raycaster?.intersectObjects([this.scene as Scene], true); + const intersects = this.raycaster?.intersectObjects( + [this.scene as Scene], + true + ); const length = intersects?.length; if (length && length > 0) { let group: Object3D | null = null; @@ -281,7 +316,7 @@ class CustomThreeLayer extends ThreeLayer{ } _getGroup(object: Object3D): Object3D | null { - if(!object){ + if (!object) { return null; } if (object.userData.acceptEvent) { @@ -296,13 +331,12 @@ class CustomThreeLayer extends ThreeLayer{ this.envMap.dispose(); this.envMap = null; } + this.cssRenderer?.domElement?.remove(); super.destroy(); this.lightTypes = null as any; this.raycaster = undefined; // this.mouse = undefined; } - - } -export default CustomThreeLayer +export default CustomThreeLayer; diff --git a/src/vue-amap-extra/packages/ThreeLayer/ThreeLayer.vue b/src/vue-amap-extra/packages/ThreeLayer/ThreeLayer.vue index ac80d50..025496c 100644 --- a/src/vue-amap-extra/packages/ThreeLayer/ThreeLayer.vue +++ b/src/vue-amap-extra/packages/ThreeLayer/ThreeLayer.vue @@ -2,101 +2,109 @@ diff --git a/test/views/three/Gltf.vue b/test/views/three/Gltf.vue index a84a84d..7240b63 100644 --- a/test/views/three/Gltf.vue +++ b/test/views/three/Gltf.vue @@ -7,43 +7,40 @@ view-mode="3D" :pitch="60" :show-building-block="false" - :features="['bg','road']" + :features="['bg', 'road']" @click="clickMap" @init="initMap" > - + - + + - - +