feat: 城市模型新增交互点和billboard提示框及相关交互.

This commit is contained in:
dragonir 2022-01-07 18:06:06 +08:00
parent 8899bc7cb3
commit cc185636a1
6 changed files with 259 additions and 8 deletions

View File

@ -318,6 +318,8 @@ module.exports = function (webpackEnv) {
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
// 路径引用 @
'@': paths.appSrc
},
plugins: [
// Prevents users from importing files from outside of src/ (or node_modules/).

View File

@ -26,7 +26,7 @@ export const makeTextSprite = (text, color, parameters) => {
}
// 创建圆形文字
export const makeCycleTextSprite = (text, W = 100, H = 100, borderWidth = 6, borderColor = 'white', color = 'black', textColor = 'white') => {
export const makeCycleTextSprite = (text, color = 'black', borderColor = 'white', textColor = 'white', W = 100, H = 100, borderWidth = 6) => {
var canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
@ -47,7 +47,7 @@ export const makeCycleTextSprite = (text, W = 100, H = 100, borderWidth = 6, bor
ctx.fillStyle = textColor;
ctx.textAlign = "center";
var metrics = ctx.measureText(text);
ctx.fillText(text, (W + borderWidth) / 2, (H + borderWidth * 2) / 2 + metrics.fontBoundingBoxDescent + metrics.actualBoundingBoxDescent * 2);
ctx.fillText(text, (W + borderWidth) / 2, (H + borderWidth * 2) / 2 + metrics.fontBoundingBoxDescent + metrics.actualBoundingBoxDescent * 4);
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
var spriteMaterial = new THREE.SpriteMaterial({

View File

@ -0,0 +1,16 @@
一、如果我们在场景图上标识一些文字有2种常用的方法
1、采用threeJs的精灵Sprite具体用法查看我另一篇博客https://my.oschina.net/u/2612473/blog/3038066
2、使用CSS2DRenderer
二、2种方法主要特征
精灵文字是在canvas中画的精灵的材质就是加载的带有文字的canvas。
CSS2DRenderer渲染器是生成一个DIV容器它的作用是能把HTML元素绑定到三维物体上,在DIV容器中各自的DOM元素分别封装到CSS2DObject的实例中并在scene中增加。
相对于精灵CSS2DRenderer有更好的灵活性可以更好的通过css控制样式并且也更方便的进行页面的跳转通过a元素
三、CSS2DRenderer方法
1getSize():返回包含宽度和长度的对象
2render ( scene : Scene, camera : Camera ) : null // 用相机渲染场景
3setSize (width : Number, height : Number) : null //设置渲染器的宽度和高度

View File

@ -27,3 +27,13 @@
0 10px 10px rgba(0,0,0,.2),
0 20px 20px rgba(0,0,0,.3);
}
.billboard {
min-width: 100px;
padding: 24px;
margin-left: 120px;
font-size: 16px;
background: rgba(0, 0, 0, .8);
border-radius: 8px;
color: #FFFFFF;
}

View File

@ -3,16 +3,27 @@ import React from 'react';
import * as THREE from "three";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { CSS2DRenderer, CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer";
import Stats from "three/examples/jsm/libs/stats.module";
import cityModel from './models/city.fbx';
import Animations from '../../assets/utils/animations';
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import './index.css';
import { makeCycleTextSprite } from '@/assets/utils/common';
export default class City extends React.Component {
constructor(props) {
super(props);
this.scene = null;
this.renderer = null;
this.labelRenderer = null;
this.city = null;
this.billboardLabel = null;
this.cityGroup = new THREE.Group;
this.interactablePoints = [
{ key: '1', value: '摩天大楼', location: { x: -2, y: 5, z: 0 } }
];
}
state = {
@ -21,12 +32,18 @@ export default class City extends React.Component {
}
componentDidMount() {
this.initThree()
this.initThree();
}
componentWillUnmount () {
this.renderer.forceContextLoss();
this.renderer.dispose();
this.scene.clear();
}
initThree = () => {
var container, controls, stats;
var camera, scene, renderer, light, cityMeshes = [];
var camera, scene, renderer, labelRenderer, light, cityMeshes = [], interactableMeshes = [];
let _this = this;
init();
animate();
@ -35,16 +52,32 @@ export default class City extends React.Component {
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
_this.renderer = renderer;
container = document.getElementById('container');
container.appendChild(renderer.domElement);
// 添加2d渲染图层
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize( window.innerWidth, window.innerHeight );
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.pointerEvents = 'none';
_this.labelRenderer = labelRenderer;
document.body.appendChild(labelRenderer.domElement);
// 场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x582424);
scene.fog = new THREE.Fog(0xeeeeee, 0, 100);
_this.scene = scene;
// 透视相机:视场、长宽比、近面、远面
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(120, 100, 100);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// threejs中采用的是右手坐标系红线是X轴绿线是Y轴蓝线是Z轴
// const axes = new THREE.AxisHelper(30);
// scene.add(axes);
// 半球光源:创建室外效果更加自然的光源
const cubeGeometry = new THREE.BoxGeometry(0.001, 0.001, 0.001);
const cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
@ -106,7 +139,18 @@ export default class City extends React.Component {
mesh.rotation.y = Math.PI / 2;
mesh.position.set(40, 0, -50);
mesh.scale.set(1, 1, 1);
scene.add(mesh);
_this.city = mesh;
_this.cityGroup.add(mesh);
// 添加交互点
_this.interactablePoints.map(item => {
let point = makeCycleTextSprite(item.key);
point.name = item.value;
point.scale.set(1, 1, 1);
point.position.set(item.location.x, item.location.y, item.location.z);
_this.cityGroup.add(point);
interactableMeshes.push(point);
})
scene.add(_this.cityGroup);
}, res => {
if (Number((res.loaded / res.total * 100).toFixed(0)) === 100) {
Animations.animateCamera(camera, controls, { x: 0, y: 10, z: 20 }, { x: 0, y: 0, z: 0 }, 4000, () => {});
@ -129,11 +173,13 @@ export default class City extends React.Component {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
stats && stats.update();
TWEEN && TWEEN.update();
controls && controls.update();
@ -142,19 +188,51 @@ export default class City extends React.Component {
// 增加点击事件声明raycaster和mouse变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseClick(event) {
function handleMouseClick(event) {
console.log(event)
// 通过鼠标点击的位置计算出raycaster所需要的点的位置以屏幕中心为原点值的范围为-1到1.
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
var intersects = raycaster.intersectObjects(cityMeshes);
var intersects = raycaster.intersectObjects(interactableMeshes);
if (intersects.length > 0) {
console.log(intersects[0].object)
let mesh = intersects[0].object
Animations.animateCamera(camera, controls, { x: mesh.position.x, y: mesh.position.y + 4, z: mesh.position.z + 12 }, { x: 0, y: 0, z: 0 }, 1200, () => {
let billboardDiv = document.createElement('div');
billboardDiv.className = 'billboard';
billboardDiv.textContent = mesh.name;
billboardDiv.style.marginTop = '1em';
let billboardLabel = new CSS2DObject(billboardDiv);
billboardLabel.position.set(0, 0, 0);
_this.billboardLabel = billboardLabel;
mesh.add(billboardLabel);
});
} else {
interactableMeshes.map(item => {
item.remove(_this.billboardLabel);
})
}
}
window.addEventListener('click', onMouseClick, false);
function handleMouseEnter(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(interactableMeshes, true);
if (intersects.length > 0) {
let mesh = intersects[0].object
mesh.material.color = new THREE.Color(0x03c03c)
} else {
interactableMeshes.map(item => {
item.material.color = new THREE.Color(0xffffff);
})
}
}
renderer.domElement.style.touchAction = 'none';
renderer.domElement.addEventListener('click', handleMouseClick, false);
renderer.domElement.addEventListener('pointermove', handleMouseEnter, false);
}
render () {

View File

@ -0,0 +1,145 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>three.js css2d - label</title>
<link type="text/css" rel="stylesheet" href="main.css">
<style>
.label {
color: #FFF;
font-family: sans-serif;
padding: 2px;
background: rgba( 0, 0, 0, .6 );
}
</style>
</head>
<body>
<div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> css2d - label</div>
<script type="module">
import * as THREE from '../build/three.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from './jsm/renderers/CSS2DRenderer.js';
let camera, scene, renderer, labelRenderer;
const clock = new THREE.Clock();
const textureLoader = new THREE.TextureLoader();
let moon;
init();
animate();
function init() {
const EARTH_RADIUS = 1;
const MOON_RADIUS = 0.27;
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 200 );
camera.position.set( 10, 5, 20 );
scene = new THREE.Scene();
const dirLight = new THREE.DirectionalLight( 0xffffff );
dirLight.position.set( 0, 0, 1 );
scene.add( dirLight );
const axesHelper = new THREE.AxesHelper( 5 );
scene.add( axesHelper );
//
const earthGeometry = new THREE.SphereGeometry( EARTH_RADIUS, 16, 16 );
const earthMaterial = new THREE.MeshPhongMaterial( {
specular: 0x333333,
shininess: 5,
map: textureLoader.load( 'textures/planets/earth_atmos_2048.jpg' ),
specularMap: textureLoader.load( 'textures/planets/earth_specular_2048.jpg' ),
normalMap: textureLoader.load( 'textures/planets/earth_normal_2048.jpg' ),
normalScale: new THREE.Vector2( 0.85, 0.85 )
} );
const earth = new THREE.Mesh( earthGeometry, earthMaterial );
scene.add( earth );
const moonGeometry = new THREE.SphereGeometry( MOON_RADIUS, 16, 16 );
const moonMaterial = new THREE.MeshPhongMaterial( {
shininess: 5,
map: textureLoader.load( 'textures/planets/moon_1024.jpg' )
} );
moon = new THREE.Mesh( moonGeometry, moonMaterial );
scene.add( moon );
//
const earthDiv = document.createElement( 'div' );
earthDiv.className = 'label';
earthDiv.textContent = 'Earth';
earthDiv.style.marginTop = '-1em';
const earthLabel = new CSS2DObject( earthDiv );
earthLabel.position.set( 0, EARTH_RADIUS, 0 );
earth.add( earthLabel );
const moonDiv = document.createElement( 'div' );
moonDiv.className = 'label';
moonDiv.textContent = 'Moon';
moonDiv.style.marginTop = '-1em';
const moonLabel = new CSS2DObject( moonDiv );
moonLabel.position.set( 0, MOON_RADIUS, 0 );
moon.add( moonLabel );
//
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize( window.innerWidth, window.innerHeight );
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
document.body.appendChild( labelRenderer.domElement );
const controls = new OrbitControls( camera, labelRenderer.domElement );
controls.minDistance = 5;
controls.maxDistance = 100;
//
window.addEventListener( 'resize', onWindowResize );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
labelRenderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
const elapsed = clock.getElapsedTime();
moon.position.set( Math.sin( elapsed ) * 5, 0, Math.cos( elapsed ) * 5 );
renderer.render( scene, camera );
labelRenderer.render( scene, camera );
}
</script>
</body>
</html>