feat: 城市模型新增交互点和billboard提示框及相关交互.
This commit is contained in:
parent
8899bc7cb3
commit
cc185636a1
|
@ -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/).
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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方法:
|
||||
(1)getSize():返回包含宽度和长度的对象
|
||||
|
||||
(2)render ( scene : Scene, camera : Camera ) : null // 用相机渲染场景
|
||||
|
||||
(3)setSize (width : Number, height : Number) : null //设置渲染器的宽度和高度
|
|
@ -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;
|
||||
}
|
|
@ -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 () {
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue