//合并热点和展览 g_currentHot = null let texLoader const playVideoWhenFlyOut = false //同时可播放的最大个数: const videoHasSound = false const playVideoMax = videoHasSound ? 1 : (window.isEdit ? 3 : browser.isMobile() ? 1 : 3) const playAniMax = window.isEdit ? 6 : browser.isMobile() ? 3 : 5 const playSyncGroup = [//需要播放同步的视频。 每次都单独定制 // ['okh1UR466371', 'LGmLHP2615503' , 'VNyBI6614896'] //中,左,右 场景SHANGJJ ] window.initHot = function (model) { var objLoader = new THREE.OBJLoader() var _planeGeometry = new THREE.PlaneGeometry(1, 1) var _boxGeometry = new THREE.BoxBufferGeometry(1, 1, 1) {//ie的mesh 加了polygonOffset也是会重叠。所以去掉前面的face: (但是突然ie又播放不了videoTexture) var newIndex = [..._boxGeometry.index.array] newIndex.splice(4 * 6, 12) _boxGeometry.setIndex(new THREE.BufferAttribute(new Uint16Array(newIndex), 1)) } var originPhotoCount = photoLoaded = originModelCount = modelLoaded = 0 var defaultTex1 = Texture.load(g_HotImage.point) var defaultTex2 = Texture.load(g_HotImage.point2) dealMap(defaultTex1, { ignoreResize: true }) dealMap(defaultTex2, { ignoreResize: true }) //这张图改为linear有黑边。 但即使不改,chrome调试手机版也有黑边和锯齿 /* var _boxMat = new THREE.MeshBasicMaterial({ color: "#eeeeee", transparent: !0, opacity: 0.8 }) */ var _boxMat = new THREE.MeshPhongMaterial({ color: "#eeeeee", transparent: !0, opacity: 0.8, side: THREE.DoubleSide }) var autoSizeInfo = /* {width2d:50}// */{ minSize: 120, maxSize: 600, nearBound: 1, farBound: 15 } var hotGroup = new THREE.Object3D; hotGroup.name = "hotGroup" model.add(hotGroup); model.hotGroup = hotGroup var animateTexSrcs = {} var getLink = function (link) { var src = '' var r = link.substring(link.indexOf("html") + 4) , o = "en" == manage.number("lang") ? "&lang=" + manage.number("lang") : ""; -1 == r.indexOf("?") ? src = link + "?time=" + randomTime().getTime() + "&id=" + window.number + o : src = link + "&time=" + randomTime().getTime() + "&id=" + window.number + o return src } var removeSrcPostMark = function (url) {//去除texture.load时自动加上的'?' var index = url.indexOf('?') if (index > -1) { return url.slice(0, index) } else return url } {//get plane Bound var planeBound = new THREE.Box3() var cornerPoint = [ new THREE.Vector3(-0.5, 0.5, 0), new THREE.Vector3(0.5, 0.5, 0), new THREE.Vector3(0.5, -0.5, 0), new THREE.Vector3(-0.5, -0.5, 0), ] cornerPoint.forEach(e => { planeBound.expandByPoint(e) }) } var shineMats = [] var getShineMat = function (texture1, texture2) { var mat = shineMats.find(e => e.uniforms.texture1.value == texture1 && e.uniforms.texture2.value == texture2) if (mat) return mat else { var mat = new THREE.ShaderMaterial({ uniforms: { opac: { type: "f", value: 0 }, texture1: { type: "t", value: texture1 }, texture2: { type: "t", value: texture2 } }, vertexShader: "varying vec2 vUv;\n\nvoid main() {\n\n vUv = uv ;\n gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n}\n", fragmentShader: "varying vec3 vNormal;\n\nvarying vec2 vUv;\n\nuniform float opac;\n\n uniform sampler2D texture1;\n\nuniform sampler2D texture2;\n\nvoid main() {\n\nvec4 tcolor1 = texture2D( texture1, vUv );\n\nvec4 tcolor2 = texture2D( texture2, vUv );\n\ngl_FragColor = mix(tcolor1,tcolor2 ,opac) + tcolor2*0.2; }\n" , transparent: !0 }) shineMats.push(mat) return mat } } var modelGeos = [] var getModelGeo = function (url) { var geo = modelGeos.find(e => e.url = url) if (geo) return geo else { var geo = null geo.url = url } } const ctlBtns = { play: Texture.load('images/soundPlay.png'), pause: Texture.load('images/soundPause.png') } const ctlBtn = new THREE.Mesh(_planeGeometry, new THREE.MeshBasicMaterial({ map: ctlBtns.pause, depthTest: false, transparent: true })) ctlBtn.name = 'ctlBtn' ctlBtn.renderOrder = 5 window.ctlBtn = ctlBtn class Hot extends THREE.Object3D { constructor(info, source) { super() this.sid = info.sid this.preDeal(info, source)// source:旧版来源 this.info = info this.cornerPoints = [] this.build(info) this.name = "hot_" + this.sid model.hots[info.sid] = this } build(info) { hotGroup.add(this) this.setFromInfo(info) } setFromInfo(info, media, objObject) { //1 恢复到编辑之前 2 初始加载 var plane = this.plane /* var transformAtPanos = {} for(var i in info.transformAtPanos){ transformAtPanos[i] = {//只保留一个位移,主要原因是大小变化和热点大小设置冲突了, isSprite和qutaernion衝突 pos : info.transformAtPanos[i].pos && info.transformAtPanos[i].pos.clone(), //qua : info.transformAtPanos[i].qua && info.transformAtPanos[i].qua.clone(), } } this.transformAtPanos = transformAtPanos */ //2024.3.18恢复transformAtPanos三个属性,isSprite优先级高于quaternion, 全局的热点大小改变不会影响当前有transformAtPanos的大小 //在每个漫游点独立设置的position。 var curPanoTransform = this.info.transformAtPanos[getTransformSid()] || {} //没有单独设置position的漫游点使用的position this.position.copy(curPanoTransform.pos || info.position) this.quaternion.copy(curPanoTransform.quaternion || info.quaternion) this.scale.copy(curPanoTransform.scale || info.scale) this.changeTexType(info.texType, media) if (!info.objSrc) { this.addPlane() if (!!this.hasBox != !!info.hasBox) { this.addBox(!this.hasBox) } } else { this.addModel(objObject || new THREE.Mesh())//暂时创建个空的 } this.updateMatrixWorld() /* if(!this.info.visiblePanos){//移到model.build时,在collider建好之后 this.getVisiblePanos() } */ {//gif if (this.animation) { GifTexDeal.remove(this.animation) } if (this.info.animateInfo && this.material_.map) { this.animation = GifTexDeal.addAnimation(this.material_.map, this, this.info.animateInfo, this.sid) this.visible && this.inSight() && GifTexDeal.start(this.animation) } } //设置visibles floor------------------ if (window.isEdit) this.setVisiblePanos() else this.getFloor() //可能需要分批计算 this.setTitleElem() this.update(player) //when edit done sprite update quaternion //------------------------------------ /* if(this.info.modelBound){ this.mesh.updateMatrixWorld() this.mesh.boxHelper2 = new THREE.Box3Helper( new THREE.Box3().copy(this.info.modelBound.bound).applyMatrix4(this.mesh.matrixWorld), new THREE.Color("#00aaee")); model.add(this.mesh.boxHelper2) }else{ var bound = new THREE.Box3() var cornerPoint = [ new THREE.Vector3(-0.5, 0.5, 0), new THREE.Vector3(0.5, 0.5, 0), new THREE.Vector3(0.5, -0.5, 0), new THREE.Vector3(-0.5, -0.5, 0), ] cornerPoint.forEach(e=>{ bound.expandByPoint(e) }) this.mesh.updateMatrixWorld() this.mesh.boxHelper2 = new THREE.Box3Helper( bound.applyMatrix4(this.mesh.matrixWorld), new THREE.Color("#00aaee")); model.add(this.mesh.boxHelper2) } */ } changeTexType(texType, media) { var plane = this.plane if (this.texType != texType || media == 'clear') { //删除旧的 if (this.texType == 'shine') { /* this.material_.uniforms.texture1.value.dispose() this.material_.uniforms.texture2.value.dispose() */ //为了恢复 不删 } else { //this.material_.map.dispose() if (this.texType == 'video') { this.texMedia && this.texMedia.pause() } else { } } if (this.material_ && !hotGroup.children.find(e => e != this && e.material_ == this.material_)) { this.material_.dispose() } //添加新的 if (texType == 'shine') { } else { this.changeMaterial(new THREE.MeshBasicMaterial({ color: "#00c8af", opacity: 0.4, transparent: !0, polygonOffset: true, //是否开启多边形偏移 //ie不开启时blank也不会闪烁 polygonOffsetFactor: -0.9, //多边形偏移因子 polygonOffsetUnits: -4.0, //多边形偏移单位 })) if (texType == 'video') { } else { } } } this.texType = texType if (media == 'clear') { this.material_.opacity = 0.4 this.texMedia = null return } if (texType == 'video') {//视频不能共用一个,否则会一起播放暂停 var video if (media) { video = media } else { if (!this.info.texSrc) return var video = $(``)[0] if (window.isEdit) video.src = manage.dealURL(this.info.texSrc) //注意,src赋值就会自动加载, preload="meta" 只加载元数据,提高加载速度,否则一开始卡 /* let src = this.info.texSrc if(window.isEdit) src = manage.dealURL(src); var video = window.videoPlayer.getVideo(src)*/ video.name = this.info.fileName } video.setAttribute("crossOrigin", 'Anonymous')//要在src设置好前解决跨域 $(video).on('contextmenu', function () { return false })//禁止右键点击出 this.texMedia = video video.oncanplaythrough = (e) => { if (this.texMedia == video) { /* if(video.paused) *///console.log({str:'oncanplaythrough '+this.sid, level:1}) //this.material_.map.needsUpdate = !0 //当初为何加这句? //this.update(player) } } /* video.onloadstart = (e)=> { console.log({str:'onloadstart '+this.sid, level:1}) } video.onloadedmetadata = (e)=> { console.log({str:'onloadedmetadata '+this.sid, level:1}) } video.onloadeddata = (e)=> { console.log({str:'onloadeddata '+this.sid, level:1}) } video.oncanplay = (e)=> { console.log({str:'oncanplay '+this.sid, level:1}) } */ if (!videoHasSound) { video.volume = 0 video.muted = true } video.currentTime = 0 if (!window.isEdit) this.material_.opacity = 0.3 this.material_.map = new THREE.VideoTexture(video) this.material_.map.wrapS = this.material_.map.wrapT = THREE.ClampToEdgeWrapping this.material_.map.generateMipmaps = true } else if (texType == 'photo') { if (media) { this.texMedia = media this.info.texSrc = media.src this.material_.map = new THREE.Texture() //texture也不能共用一个,因为有的会有动画,就不一样 this.material_.map.image = media //image可以共用 this.material_.map.needsUpdate = !0 this.material_.opacity = 1 } else { if (!this.info.texSrc) return this._loadDones = [] this.material_.opacity = 0.1 } } else if (texType == 'shine') { if (media) { this.changeShineTex(media) } else { this.styleImg = /* this.styleImg || */[] if (this.info.styleImg) { this.styleImg = this.info.styleImg.map((src) => { return { src:/* manage.dealURL( */src } //如果要dealURL,在predeal里 }) } this.changeShineTex(this.styleImg) } this.info.texSrc = null } if (this.material_.map) { /* this.material_.map.minFilter = THREE.LinearFilter; this.material_.map.magFilter = THREE.LinearFilter; */ dealMap(this.material_.map) this.material_.color.set("#FFFFFF") this.material_.needsUpdate = true } } changeShineTex(styleImg) { styleImg = styleImg || this.styleImg var tex1, tex2 if (styleImg.length) { tex1 = Texture.load(styleImg[0].src) tex2 = Texture.load(styleImg[1] && styleImg[1].src || styleImg[0].src) dealMap(tex1, { ignoreResize: true }) dealMap(tex2, { ignoreResize: true }) } else { tex1 = defaultTex1 tex2 = defaultTex2 } this.changeMaterial(getShineMat(tex1, tex2)) this.styleImg = styleImg this.info.styleImg = styleImg.map(img => img.src) } changeMaterial(mat) { this.material_ = mat this.mesh && this.mesh.traverse((mesh) => { if (mesh.material && !(mesh instanceof THREE.Box3Helper)) { mesh.material = this.material_ } }) } preDeal(info, source) {// source:来源 var convertValue = function (v, Type) { var value if (v instanceof Array) { v.forEach((v1) => { v1 = parseFloat(v1) }) value = new Type().fromArray(v) } else { if (!(v instanceof Type)) { for (let i in v) { v[i] = parseFloat(v[i]) } value = new Type().copy(v) } else { value = v } } return value } if (!info.transformAtPanos) info.transformAtPanos = {} if (source == 'byHot') { var infoAttribute = info.infoAttribute || {} info.title = infoAttribute.title || info.title info.model = infoAttribute.model || info.model || [] //模型链接 info.images = infoAttribute.images || info.images || [] info.video = infoAttribute.video || info.video || [] info.bgName = infoAttribute.bgName || info.bgName info.backgroundMusic = info.backgroundMusic || info.backgroundMusic info.iframe = infoAttribute.iframe || info.iframe || [] info.styleImg = infoAttribute.styleImg || info.styleImg || [] info.content = infoAttribute.content || info.content { let action = CloneObject(settings.hotClickEvent.shine) if (info.actionType == 'noAction' || info.noAction) { action.examine = false, action.openHot = false } else if (info.actionType == 'dontExam') { action.examine = false } info.actionType = action } if (info.quaternion) { info.quaternion = convertValue(info.quaternion, THREE.Quaternion) } else { info.rotation = convertValue(info.rotation, THREE.Vector3) info.quaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler().setFromVector3(info.rotation)) //热点的旧数据很多是字符串 } var s = Hot.getDefaulScale(info.hotIconScale) //旧版的大小,统一转换成新版 info.scale = new THREE.Vector3(s, s, 0.02) delete info.infoAttribute /* for (let i in this.transformAtPanos) { info.transformAtPanos[i].pos = new THREE.Vector3().fromArray(info.transformAtPanos[i].pos) info.transformAtPanos[i].qua && (info.transformAtPanos[i].qua = new THREE.Quaternion().fromArray(info.transformAtPanos[i].qua)) } */ info.texType = "shine" } else { if (source == 'byOverlay') { info.texType = info.media[0] //info.title = info.texType == 'video'?'视频':'图片' info.texSrc = info.file info.actionType = CloneObject(settings.hotClickEvent[info.texType])//给一个默认 delete info.media info.quaternion = convertValue(info.qua, THREE.Quaternion) info.position = info.pos delete info.pos delete info.qua let a = info.texSrc.split('/') info.fileName = a.pop() info.scale = new THREE.Vector3( info.width, info.height, info.depth ) delete info.width; delete info.height; delete info.depth delete info.file } else { info.quaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler().fromArray(info.rotation))//.setFromVector3(info.rotation) } info.model = info.model || [] //模型链接 info.images = info.images || [] info.video = info.video || [] info.iframe = info.iframe || [] info.styleImg = info.styleImg || [] info.imagesDesc = info.imagesDesc || [] info.videosDesc = info.videosDesc || [] info.titleShowType = info.titleShowType || 'hover' info.titlePos = info.titlePos || 'right' } if (info.texSrc) { info.texSrc = manage.removeSrcPostMark(info.texSrc) } //whole: //为了兼容旧数据,尽量和hot的数据靠近,最后保存在hot里 info.position = convertValue(info.position, THREE.Vector3) info.scale = convertValue(info.scale, THREE.Vector3) delete info.rotation //暂时不用,只有保存时才存为roration info.linkType = info.linkType || "common" for (let i in info.transformAtPanos) { info.transformAtPanos[i].pos = new THREE.Vector3().fromArray(info.transformAtPanos[i].pos) info.transformAtPanos[i].qua && (info.transformAtPanos[i].qua = new THREE.Quaternion().fromArray(info.transformAtPanos[i].qua)) info.transformAtPanos[i].scale && (info.transformAtPanos[i].scale = new THREE.Vector3().fromArray(info.transformAtPanos[i].scale)) } } addBox(state) { if (state == !!this.hasBox) { return } if (state) { var box = new THREE.Mesh(_boxGeometry, _boxMat) box.position.set(0, 0, 1 / 2) box.renderOrder = 3 this.plane.position.set(0, 0, 1) this.add(box) this.box = box } else { this.plane.position.set(0, 0, 0) this.remove(this.box) this.box = null } this.hasBox = this.info.hasBox = state } /* getSizeByScale() { return { width: settings.defaultOverlaySize[0] * this.scale.x, height: settings.defaultOverlaySize[1] * this.scale.y } } getScaleBySize(width, height) { return { x: width / settings.defaultOverlaySize[0], y: height / settings.defaultOverlaySize[1], } }*/ setVisiblePanos(visibleData) { if (visibleData) { this.info.visiblePanos = visibleData } else if (!this.info.visiblePanos) { this.getVisiblePanos() } this.getFloor() } getFloor() {//用于飞出后分层展示。最好只属于一个楼层,且是贴得最近的 //先直接向下探测 //不准的话再向四周选三个方向,以及visiblePanos,来加成求分 if (player.model.floors.list.length <= 1) return if (this.floorByRay) { return this.floor = this.floorByRay //重复getFloor的话,因模型不会变,用ray得到的floor不会变 } let result if (this.floorByRay !== false) { let map = new Map let downVec = new THREE.Vector3(0, -1, 0) let pos = this.info.transformAtPanos.outSide?.pos || this.position let centerPos if (!this.plane) { let bound = new THREE.Box3().copy(this.info.modelBound.bound) let center = bound.center() let qua = this.info.transformAtPanos.outSide?.qua || this.quaternion let scale = this.info.transformAtPanos.outSide?.scale || this.scale var matrixWorld = new THREE.Matrix4().compose(pos, qua, scale) matrixWorld.multiplyMatrices(matrixWorld, this.mesh.matrix) centerPos = center.applyMatrix4(matrixWorld) } else { centerPos = pos } let request = [(floor) => { player.raycaster.set(centerPos, downVec) let n = player.raycaster.intersectObjects(floor.collider.children, true) if (n && n[0]) { map.set(floor, n[0].distance) return true } }] let rank = [(floor) => { return - map.get(floor) }] result = common.sortByScore(player.model.floors.list, request, rank) } if (result) { this.floor = this.floorByRay = result[0].item } else { this.floorByRay = false //标记 使用ray无法找到 console.warn('热点通过raycaster没有找到楼层', this) if (this.info.visiblePanos) { let floorIndex = this.info.visiblePanos.map(e => player.model.panos.index[e].floorIndex).sort()[Math.floor(this.info.visiblePanos.length / 2)]//中位数 this.floor = player.model.floors.index[floorIndex] console.log('热点根据visiblePanos找到楼层', this, this.floor.name) } else { this.floor = null } } } getVisiblePanos() {//在不同点还不一样 var depth = this.hasBox ? this.scale.z : 0 var width = this.scale.x, height = this.scale.y var cornerPoint if (this.plane) { cornerPoint = [ new THREE.Vector3(0, 0, depth), new THREE.Vector3(-width / 2, height / 2, depth), new THREE.Vector3(width / 2, height / 2, depth), new THREE.Vector3(width / 2, -height / 2, depth), new THREE.Vector3(-width / 2, -height / 2, depth), ] } else { var bound = new THREE.Box3().copy(this.info.modelBound.bound) var center = bound.center() cornerPoint = [ new THREE.Vector3(center.x, center.y, center.z), new THREE.Vector3(bound.min.x, bound.min.y, bound.min.z), new THREE.Vector3(bound.min.x, bound.min.y, bound.max.z), new THREE.Vector3(bound.min.x, bound.max.y, bound.min.z), new THREE.Vector3(bound.max.x, bound.min.y, bound.min.z), new THREE.Vector3(bound.max.x, bound.max.y, bound.min.z), new THREE.Vector3(bound.max.x, bound.min.y, bound.max.z), new THREE.Vector3(bound.min.x, bound.max.y, bound.max.z), new THREE.Vector3(bound.max.x, bound.max.y, bound.max.z), ] } var getPos = (position, quaternion = this.info.quaternion, scale = this.info.scale) => {//每个overlay位置对应5个坐标,plane中心和四个角的位置 if (this.plane) { return cornerPoint.map(e => { return e.clone().applyQuaternion(this.info.quaternion).add(position) }) } else { var matrixWorld = new THREE.Matrix4().compose(position, quaternion, scale) matrixWorld.multiplyMatrices(matrixWorld, this.mesh.matrix) return cornerPoint.map(e => { return e.clone().applyMatrix4(matrixWorld) }) } } this.info.visiblePanos = [] var customPositions = getPos(this.info.position) var posAtPanos = {} for (let panoId in this.info.transformAtPanos) { if (panoId == 'outSide') continue let { pos, qua, scale } = this.info.transformAtPanos[panoId] posAtPanos[panoId] = getPos(pos, qua, scale) } let maxCount = browser.isMobile() ? 2000 : 5000 let possiblePanos = model.panos.list if (this.plane && !this.info.isSprite) {//目前都是单面,所以只要可以看到的一面的热点 let defaultDir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.info.quaternion).negate() possiblePanos = possiblePanos.filter(pano => { let dir1 if (this.info.transformAtPanos[pano.id]?.qua) { dir1 = new THREE.Vector3(0, 0, -1).applyQuaternion(this.info.transformAtPanos[pano.id].qua).negate() } else { dir1 = defaultDir } let dir2 = new THREE.Vector3().subVectors(pano.position, this.position).normalize() return dir1.dot(dir2) > 0 }) } let c = model.panos.list.length * model.colliders.length if (window.isEdit || c < maxCount) { //编辑页面保险起见还是全部算完后才可浏览,就能保证保存全部的visiblePano this.info.visiblePanos = common.getVisiblePano(customPositions, possiblePanos, { model: model.colliders, posAtPanos }) } else { let start = 0 let interval = setInterval(() => { let end = start + Hot.visiPanosCountSlice end = Math.min(end, possiblePanos.length) let i = start start = end let panos = possiblePanos.slice(i, end) this.info.visiblePanos = this.info.visiblePanos.concat(common.getVisiblePano(customPositions, panos, { model: model.colliders, posAtPanos })) if (end >= possiblePanos.length) { //console.log(window.hotsi ?(++window.hotsi): (window.hotsi = 1)) this.getFloor() //re get clearInterval(interval) } }, Hot.visiEveryDurSlice)// visiEveryDurSlice 等在main中定义 } } updateVisible(panos, visibility, type) { if (window.isEdit && editTool.hotpoint.editSpot == this || player.mode != 'panorama' && visibility !== false) { return convertTool.updateVisible(this, 'visi', true) } let visible = visibility != void 0 ? visibility : (!this.info.visiblePanos || (type == 'every' ? panos.every(pano => this.info.visiblePanos.includes(pano.id)) : panos.some(pano => this.info.visiblePanos.includes(pano.id))))//type为every时,需要panos中每个都可见才显示 convertTool.updateVisible(this, 'visi', visible) this.titleElem && this.titleElem.setVisible(this.visible, 'hotVisible', 1) if (this.texType == 'video') { //this.switchPlay(this.visible, this.visible ? null : 'stop' );//可见时不操作;不可见时停止 this.update(player) } } getWorldCenter() { return this.getBoundOri().center().applyMatrix4(this.mesh.matrixWorld) } getBoundOri() { let bound if (this.objObject) { bound = new THREE.Box3().copy(this.info.modelBound.bound) } else { bound = planeBound.clone() } return bound } getCornerPoint() {//获取在每个漫游点上的视觉边界点 可以打开boxHelper和addBall来观测是否准确 if (this.cornerPoints[player.currentPano.id]) { return this.cornerPoints[player.currentPano.id] } else { var boundPoint, cornerPoint var center//中心点 if (this.plane) { center = this.plane.getWorldPosition() boundPoint = [ new THREE.Vector3(-0.5, 0.5, 0), new THREE.Vector3(0.5, 0.5, 0), new THREE.Vector3(0.5, -0.5, 0), new THREE.Vector3(-0.5, -0.5, 0), ] } else { var bound = new THREE.Box3().copy(this.info.modelBound.bound) boundPoint = [ new THREE.Vector3(bound.min.x, bound.min.y, bound.min.z), new THREE.Vector3(bound.min.x, bound.min.y, bound.max.z), new THREE.Vector3(bound.min.x, bound.max.y, bound.min.z), new THREE.Vector3(bound.max.x, bound.min.y, bound.min.z), new THREE.Vector3(bound.max.x, bound.max.y, bound.min.z), new THREE.Vector3(bound.max.x, bound.min.y, bound.max.z), new THREE.Vector3(bound.min.x, bound.max.y, bound.max.z), new THREE.Vector3(bound.max.x, bound.max.y, bound.max.z), ] } var maxLon = -Infinity var minLon = +Infinity var maxLat = -Infinity var minLat = +Infinity var pos1 = player.currentPano.position.clone() center = this.position.clone() //模型bound的中心点已经位移到了hot中心点。 注意不能用getWorldPosition,得到的会是偏移的 var dir = center.clone().sub(pos1).normalize() var centerDirInfo = {} player.cameraControls.controls.panorama.lookAt.call(centerDirInfo, null, dir) boundPoint.forEach(e => {//lon左右 var point = e.applyMatrix4(this.mesh.matrixWorld) var dir = point.clone().sub(pos1).normalize() var dirInfo = {} player.cameraControls.controls.panorama.lookAt.call(dirInfo, null, dir) var diffLon = (dirInfo.lon - centerDirInfo.lon) % 360 if (Math.abs(diffLon) > 180) {//因为有时需要根据符号判断是在中心的左边还是右边,所以限制在180内 diffLon += (diffLon > 0 ? -360 : 360) } var diffLat = dirInfo.lat - centerDirInfo.lat maxLon = Math.max(diffLon, maxLon) minLon = Math.min(diffLon, minLon) maxLat = Math.max(diffLat, maxLat) minLat = Math.min(diffLat, minLat) }) var diffLon = maxLon - minLon var diffLat = maxLat - minLat if (diffLat > 180) {//可能是到了反面。不好算,直接返回所有boundPoint cornerPoint = boundPoint } else { //读取lon lat的最大最小值,勾勒出一个没有倾斜的矩形 。它比boundPoint看起来范围更大些 maxLon = maxLon + centerDirInfo.lon maxLat = maxLat + centerDirInfo.lat minLon = minLon + centerDirInfo.lon minLat = minLat + centerDirInfo.lat var dirs = [ math.getDirByLonLat(maxLon, maxLat), math.getDirByLonLat(minLon, minLat), math.getDirByLonLat(maxLon, minLat), math.getDirByLonLat(minLon, maxLat), ] cornerPoint = dirs.map(e => { return e.clone().add(pos1) }) cornerPoint = [center, ...cornerPoint] //最后增加一个中心点 } /* if(this.objObject){ cornerPoint = [pos2, ...cornerPoint] } */ //addPoints(cornerPoint) this.cornerPoints[player.currentPano.id] = { cornerPoint, diffLon, diffLat } return this.cornerPoints[player.currentPano.id] } } getMediaSize() { let size = new THREE.Vector2 if (this.texMedia) { if (this.texType == 'photo') { size.x = this.texMedia.width size.y = this.texMedia.height //动画的话再变 if (this.info.animateInfo) { size.x /= this.info.animateInfo.cellXcount size.y /= this.info.animateInfo.cellYcount } } else { size.x = this.texMedia.videoWidth || this.videoWidth size.y = this.texMedia.videoHeight || this.videoHeight } } return size } inSight() { //return true if (window.isEdit) return true // 太容易move了 if (player.mode == 'panorama' && player.currentPano) { if (!player.camera) return var cornerPointInfo = this.getCornerPoint() var cornerPoint let min = new THREE.Vector2(5, 5) let scaleRatio = 1 / player.zoomLevel /* //根据media原始大小来调整阈值: media的原始大小能代表期望显示的大小,如果显示大小的远小于期望大小,就不显示(此时能感受到贴图锯齿严重,清晰度被浪费)。比如如果gif是一个很小的按钮,即使diffLon很小也要显示。缺点:需要用户根据所需上传合适清晰度的图。 let size = this.getMediaSize() if(size.x>0){ scaleRatio *= Math.sqrt(size.x * size.y) / 1000 } min.multiplyScalar(scaleRatio) 2025.2:不行,这样大的视频贴上去很容易返回false */ //gif如果是一个按钮,需要更小的lon . 最好是能有滑块调节可视 if (this.animation) { min.multiplyScalar(0.5) } if (cornerPointInfo.diffLon < min.x || cornerPointInfo.diffLat < min.y) { //console.log('two far and small') return false } if (cornerPointInfo.diffLon < 15 && cornerPointInfo.diffLat < 15) {//当很小的时候,只判断中心点即可 cornerPoint = [cornerPointInfo.cornerPoint[0]] } else { cornerPoint = cornerPointInfo.cornerPoint } for (let i = 0, j = cornerPoint.length; i < j; i++) {//只要有一点可见就算看见 var pos2d = math.getPos2d(cornerPoint[i], player.camera, $("#player")[0]) if (pos2d.trueSide && pos2d.inSight) return true } } else {//飞出 只判断在不在画面内 //return playVideoWhenFlyOut//true let frustumMatrix = new THREE.Matrix4 frustumMatrix.multiplyMatrices(player.camera.projectionMatrix, player.camera.matrixWorldInverse) let frustum = new THREE.Frustum() frustum.setFromMatrix(frustumMatrix) let bound = this.getBoundOri() bound.applyMatrix4(this.matrixWorld) return frustum.intersectsBox(bound) } } update(player) { if (this.info.isSprite) { this.quaternion.copy(player.camera.quaternion) } this.updateScale() this.updateTitle() } updateScale(e, t) {//自适应调节大小 if (!DATA.autoAdjustHotScale || this.texType != 'shine' || !this.plane) return var scale = convertTool.getScaleForConstantSize($.extend({}, autoSizeInfo, { position: this.position.clone() })) this.plane.scale.set(scale, scale, scale) //修改mesh,和自定义修改的scale不冲突 } switchPlay(state) {//手动播放暂停 this.pausedByUser = !state this.videoControl(state) } loadVideo(video) { if (video.src_) return video.src = video.src_ = manage.dealURL(this.info.texSrc) } videoControl(state) { if (this.texType != "video" || !this.material_.map) return var video = this.texMedia this.shouldPlay = state if (!state || state == 'stop') { if (!video.paused) { video.pause() if (!video.muted) { SoundManager.pause('hot', true)//自动播放被中断的音频 (bgm } console.log({ str: "paused " + this.sid, level: 1 }) } /* video.lastCurTime = state == 'stop' ? 0 : video.currentTime //记录 video.src = video.src_ = '' */ if (state == 'stop') { video.currentTime = 0 } if (this.sid == 'LezWqUp088015') { ctlBtn.material.map = ctlBtns.play } } else if (state) { if (/* !isVideoPlayed(video) */ video.paused) { console.log({ str: 'videoControl play ' + ", " + this.sid, level: 1 }) this.loadVideo(video) video.play() if (!video.muted) { SoundManager.play('hot') //暂停bgm等 } //video.currentTime = video.lastCurTime || 0 this.changeOpaWhenPlay(video) if (this.sid == 'LezWqUp088015') { ctlBtn.material.map = ctlBtns.pause } //处理同步播放 let group = playSyncGroup.find(e => e.includes(this.sid)) if (group) { let others = group.filter(e => e != this.sid).map(e => player.model.hots[e].texMedia) //console.log('controlVideo play', this.id, 'other currentTime', others.map(e=>e.currentTime)) video.currentTime = others[0].currentTime } //if(isVideoPlayed(video))console.log({str:"played " + this.sid + video.duration ,level:1}) } } } changeOpaWhenPlay(video) {//当播放成功后,恢复为不透明 if (isNaN(video.duration)) {//未加载好 return setTimeout(() => { this.changeOpaWhenPlay(video) }, 100) } const minPlayedTime = Math.min(video.duration / 10, 0.1) //到这个时间说明加载成功 if (!video.hasInitedOpacity) { setTimeout(e => { //console.log('trychangeOpa ',this.sid,video.currentTime) if (video.currentTime > minPlayedTime) { this.material_.opacity = 1 video.hasInitedOpacity = true //console.log('changeOpaWhenPlay',this.sid) } else { if (!video.paused) {//再次尝试 return setTimeout(() => { this.changeOpaWhenPlay(video) }, 500) } } }, minPlayedTime * 1000 + 500) } } /* setDefaultHotScale = function(){//设置成默认热点大小 var w = DATA.hotIconScale) * g_HotMeshSize.g_HotMeshWidth this.scale.set( w, w, this.scale.z) } */ setTitleElem() { var title = this.info.title if (title) { if (!this.titleElem) { this.titleElem = new Label2D({ position: this.position, innerHTML: `