diff --git a/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue b/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue index c65bf75d0df2c35a16b15170e9cc89d4ab14ca6f..989f5e426f69fcd1cedc2657b29a520121597968 100644 --- a/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue +++ b/src/vue-amap-extra/packages/ThreeGltf/ThreeGltf.vue @@ -1,17 +1,23 @@ diff --git a/src/vue-amap-extra/packages/ThreeLayer/CSS3DRenderer.js b/src/vue-amap-extra/packages/ThreeLayer/CSS3DRenderer.js new file mode 100644 index 0000000000000000000000000000000000000000..28ad64e38fbe354d5c7bee2914336f2a8eef04c0 --- /dev/null +++ b/src/vue-amap-extra/packages/ThreeLayer/CSS3DRenderer.js @@ -0,0 +1,322 @@ +import { Matrix4, Object3D, Quaternion, Vector3 } from 'three'; + +/** + * Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs + */ + +const _position = new Vector3(); +const _quaternion = new Quaternion(); +const _scale = new Vector3(); + +class CSS3DObject extends Object3D { + constructor(element = document.createElement('div')) { + super(); + + this.isCSS3DObject = true; + + this.element = element; + this.element.style.position = 'absolute'; + this.element.style.pointerEvents = 'auto'; + this.element.style.userSelect = 'none'; + + this.element.setAttribute('draggable', false); + + 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); + + this.element = source.element.cloneNode(true); + + return this; + } +} + +class CSS3DSprite extends CSS3DObject { + constructor(element) { + super(element); + + this.isCSS3DSprite = true; + + this.rotation2D = 0; + } + + copy(source, recursive) { + super.copy(source, recursive); + + const self = this; + + self.rotation2D = source.rotation2D; + + return this; + } +} + +// + +const _matrix = new Matrix4(); +const _matrix2 = new Matrix4(); + +class CSS3DRenderer { + constructor(parameters = {}) { + const _this = this; + + let _width, _height; + let _widthHalf, _heightHalf; + + const cache = { + camera: { style: '' }, + objects: new WeakMap(), + }; + + const domElement = + parameters.element !== undefined ? parameters.element : document.createElement('div'); + + domElement.style.overflow = 'hidden'; + + this.domElement = domElement; + + const viewElement = document.createElement('div'); + viewElement.style.transformOrigin = '0 0'; + viewElement.style.pointerEvents = 'none'; + domElement.appendChild(viewElement); + + const cameraElement = document.createElement('div'); + + cameraElement.style.transformStyle = 'preserve-3d'; + + viewElement.appendChild(cameraElement); + + this.getSize = function () { + return { + width: _width, + height: _height, + }; + }; + + this.render = function (scene, camera) { + const fov = camera.projectionMatrix.elements[5] * _heightHalf; + + if (camera.view && camera.view.enabled) { + // view offset + viewElement.style.transform = `translate( ${ + -camera.view.offsetX * (_width / camera.view.width) + }px, ${-camera.view.offsetY * (_height / camera.view.height)}px )`; + + // view fullWidth and fullHeight, view width and height + viewElement.style.transform += `scale( ${camera.view.fullWidth / camera.view.width}, ${ + camera.view.fullHeight / camera.view.height + } )`; + } else { + viewElement.style.transform = ''; + } + + if (scene.matrixWorldAutoUpdate === true) scene.updateMatrixWorld(); + if (camera.parent === null && camera.matrixWorldAutoUpdate === true) + camera.updateMatrixWorld(); + + let tx, ty; + + if (camera.isOrthographicCamera) { + tx = -(camera.right + camera.left) / 2; + ty = (camera.top + camera.bottom) / 2; + } + + const scaleByViewOffset = + camera.view && camera.view.enabled ? camera.view.height / camera.view.fullHeight : 1; + const cameraCSSMatrix = camera.isOrthographicCamera + ? `scale( ${scaleByViewOffset} )` + + 'scale(' + + fov + + ')' + + 'translate(' + + epsilon(tx) + + 'px,' + + epsilon(ty) + + 'px)' + + getCameraCSSMatrix(camera.matrixWorldInverse) + : `scale( ${scaleByViewOffset} )` + + 'translateZ(' + + fov + + 'px)' + + getCameraCSSMatrix(camera.matrixWorldInverse); + const perspective = camera.isPerspectiveCamera ? 'perspective(' + fov + 'px) ' : ''; + + const style = + perspective + cameraCSSMatrix + 'translate(' + _widthHalf + 'px,' + _heightHalf + 'px)'; + + if (cache.camera.style !== style) { + cameraElement.style.transform = style; + + cache.camera.style = style; + } + + renderObject(scene, scene, camera, cameraCSSMatrix); + }; + + 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'; + + viewElement.style.width = width + 'px'; + viewElement.style.height = height + 'px'; + + cameraElement.style.width = width + 'px'; + cameraElement.style.height = height + 'px'; + }; + + function epsilon(value) { + return Math.abs(value) < 1e-10 ? 0 : value; + } + + function getCameraCSSMatrix(matrix) { + const elements = matrix.elements; + + return ( + 'matrix3d(' + + epsilon(elements[0]) + + ',' + + epsilon(-elements[1]) + + ',' + + epsilon(elements[2]) + + ',' + + epsilon(elements[3]) + + ',' + + epsilon(elements[4]) + + ',' + + epsilon(-elements[5]) + + ',' + + epsilon(elements[6]) + + ',' + + epsilon(elements[7]) + + ',' + + epsilon(elements[8]) + + ',' + + epsilon(-elements[9]) + + ',' + + epsilon(elements[10]) + + ',' + + epsilon(elements[11]) + + ',' + + epsilon(elements[12]) + + ',' + + epsilon(-elements[13]) + + ',' + + epsilon(elements[14]) + + ',' + + epsilon(elements[15]) + + ')' + ); + } + + function getObjectCSSMatrix(matrix) { + const elements = matrix.elements; + const matrix3d = + 'matrix3d(' + + epsilon(elements[0]) + + ',' + + epsilon(elements[1]) + + ',' + + epsilon(elements[2]) + + ',' + + epsilon(elements[3]) + + ',' + + epsilon(-elements[4]) + + ',' + + epsilon(-elements[5]) + + ',' + + epsilon(-elements[6]) + + ',' + + epsilon(-elements[7]) + + ',' + + epsilon(elements[8]) + + ',' + + epsilon(elements[9]) + + ',' + + epsilon(elements[10]) + + ',' + + epsilon(elements[11]) + + ',' + + epsilon(elements[12]) + + ',' + + epsilon(elements[13]) + + ',' + + epsilon(elements[14]) + + ',' + + epsilon(elements[15]) + + ')'; + + return 'translate(-50%,-50%)' + matrix3d; + } + + function renderObject(object, scene, camera, cameraCSSMatrix) { + if (object.isCSS3DObject) { + const visible = object.visible === true && object.layers.test(camera.layers) === true; + object.element.style.display = visible === true ? '' : 'none'; + + if (visible === true) { + object.onBeforeRender(_this, scene, camera); + + let style; + + if (object.isCSS3DSprite) { + // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/ + + _matrix.copy(camera.matrixWorldInverse); + _matrix.transpose(); + + if (object.rotation2D !== 0) + _matrix.multiply(_matrix2.makeRotationZ(object.rotation2D)); + + object.matrixWorld.decompose(_position, _quaternion, _scale); + _matrix.setPosition(_position); + _matrix.scale(_scale); + + _matrix.elements[3] = 0; + _matrix.elements[7] = 0; + _matrix.elements[11] = 0; + _matrix.elements[15] = 1; + + style = getObjectCSSMatrix(_matrix); + } else { + style = getObjectCSSMatrix(object.matrixWorld); + } + + const element = object.element; + const cachedObject = cache.objects.get(object); + + if (cachedObject === undefined || cachedObject.style !== style) { + element.style.transform = style; + + const objectData = { style: style }; + cache.objects.set(object, objectData); + } + + if (element.parentNode !== cameraElement) { + cameraElement.appendChild(element); + } + + object.onAfterRender(_this, scene, camera); + } + } + + for (let i = 0, l = object.children.length; i < l; i++) { + renderObject(object.children[i], scene, camera, cameraCSSMatrix); + } + } + } +} + +export { CSS3DObject, CSS3DSprite, CSS3DRenderer }; diff --git a/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts b/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts index ca87abc761e4f34ce31ac0df37f07f7eecde0876..b8c349c5c640e5d60a6f7a912cb4b41d4ff86f91 100644 --- a/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts +++ b/src/vue-amap-extra/packages/ThreeLayer/CustomThreeLayer.ts @@ -21,12 +21,13 @@ 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"; +import { CSS3DRenderer } from "./CSS3DRenderer"; interface Options extends ThreeLayerOptions { lights?: LightOption[]; // 灯光数组 hdr?: HDROptions; // 开启HDR配置 axesHelper: boolean; // 是否开启箭头,用于debug,默认不开启 - createCssRender?: boolean; //是否创建CssRender + createCssRender: boolean; //是否创建CSSRender } class CustomThreeLayer extends ThreeLayer { @@ -50,7 +51,8 @@ class CustomThreeLayer extends ThreeLayer { passList = [] as any[]; clock = new Clock(); preHoverGroup = null as Object3D | null; - cssRenderer = null as CSS2DRenderer | null; + css2DRenderer = null as CSS2DRenderer | null; + css3DRenderer = null as CSS3DRenderer | null; constructor(map: any, options: Options, callback: () => void) { options.onInit = (render, scene) => { @@ -60,7 +62,7 @@ class CustomThreeLayer extends ThreeLayer { scene.add(axesHelper); } this.renderer?.setPixelRatio(window.devicePixelRatio); - if (options.createCssRender) this.creatCssRender(map); + if (options.createCssRender) this.creatCssRenders(map); this.createEffect(); this.createLights(options.lights || []); this.createHDR(options.hdr); @@ -74,7 +76,8 @@ class CustomThreeLayer extends ThreeLayer { 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); + this.css2DRenderer?.render(this.scene as Scene, camera as Camera); + this.css3DRenderer?.render(this.scene as Scene, camera as Camera); } }; super(map, options); @@ -85,7 +88,8 @@ class CustomThreeLayer extends ThreeLayer { 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); + this.css2DRenderer?.setSize(size.x, size.y); + this.css3DRenderer?.setSize(size.x, size.y); } } @@ -93,19 +97,26 @@ class CustomThreeLayer extends ThreeLayer { 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); + this.css2DRenderer?.setSize(size?.x as number, size?.y as number); + this.css3DRenderer?.setSize(size?.x as number, size?.y as number); // const renderPass = new ThreeRenderPass( this.scene, this.camera ); // this.renderPass = renderPass; // this.effectComposer.addPass(renderPass); } - creatCssRender(map: any) { + creatCssRenders(map: any) { + this.css2DRenderer = new CSS2DRenderer(); + this.css2DRenderer.domElement.style.position = "absolute"; + this.css2DRenderer.domElement.style.left = "0"; + this.css2DRenderer.domElement.style.top = "0"; + + this.css3DRenderer = new CSS3DRenderer(); + this.css3DRenderer.domElement.style.position = "absolute"; + this.css3DRenderer.domElement.style.left = "0"; + this.css3DRenderer.domElement.style.top = "0"; 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); + element.appendChild(this.css2DRenderer.domElement); + element.appendChild(this.css3DRenderer.domElement); } addPass(pass: any) { @@ -331,7 +342,8 @@ class CustomThreeLayer extends ThreeLayer { this.envMap.dispose(); this.envMap = null; } - this.cssRenderer?.domElement?.remove(); + this.css2DRenderer?.domElement?.remove(); + this.css3DRenderer?.domElement?.remove(); super.destroy(); this.lightTypes = null as any; this.raycaster = undefined; diff --git a/test/router/index.ts b/test/router/index.ts index 18464d30673245157dca60e99b2b6a431ee39fea..1d004acfa6f227d55b195f68fa8c31fe8bf357fd 100644 --- a/test/router/index.ts +++ b/test/router/index.ts @@ -43,6 +43,7 @@ export const routes = [ {path: '/vector/rectangle',name: 'vector矩形', component: () => import('../views/vector/Rectangle.vue')}, {path: '/layer/tiles3D',name: '3DTiles图层', component: () => import('../views/layer/Tiles3D.vue')}, {path: '/three/gltf',name: 'ThreeJS Gltf示例', component: () => import('../views/three/Gltf.vue')}, + {path: '/three/gltf_popup',name: 'ThreeJS Gltf示例(Popup)', component: () => import('../views/three/Gltf_Popup.vue')}, {path: '/three/threeVideo',name: 'ThreeJS 视频', component: () => import('../views/three/ThreeVideo.vue')}, {path: '/three/polygon',name: 'ThreeJS面图层', component: () => import('../views/three/Polygon.vue')}, {path: '/three/3dtiles',name: 'ThreeJS 3Dtiles图层', component: () => import('../views/three/Tiles3d.vue')}, diff --git a/test/views/three/Gltf.vue b/test/views/three/Gltf.vue index e0031afb41fc91bc0ded94c92659e6e24d1389f1..74eecaf537b65a5d61128cf23e50c5fb27134a4b 100644 --- a/test/views/three/Gltf.vue +++ b/test/views/three/Gltf.vue @@ -23,7 +23,6 @@ :alpha="true" :antialias="true" :create-canvas="false" - :create-css-render="true" @init="initLayer" @click="clickLayer" @mouseover="mouseoverLayer" @@ -76,22 +75,8 @@ :angle="carAngle" :rotation="rotation" :move-animation="moveAnimation" - :show-popup="popupVisible" - :popup-height="popupHeight" @init="initCar" > -
- 测试三维信息弹窗 -
+
+ + + + + + + + + +
+ 测试GLTF信息弹窗(CSS3DRenderer) +
+
+ + +
+ 测试GLTF信息弹窗(CSS2DRenderer) +
+
+
+
+
+ + {{ popupVisible ? "隐藏Popup" : "显示Popup" }} + + popup升高 + popup方大 + 停止动画 + 开始动画 + 停止车辆 + 移动车辆 +
+
+ + + + +