先看看基础效果叭
demo地址
前言
本章我们将做一个可拖拽,开始时自传,点击城市可以动的地球demo
首先需要知道什么是 three.js。 简单的说,three.js 是一个非常优秀的 WebGL 开源框架, three(3d) + js(javaScript)--了解 three.js。
而 WebGL 是在浏览器中实现三维效果的一套规范。
在 three.js 中有三大大重要的概念需要先了解一下:
- 场景(scene)
- 相机(camera)
- 渲染器(renderer)
1、scene
在 WebGL 世界里,场景是一个非常重要的概念,它是存放所有物体的容器。在 three.js 里面新建一个场景很简单,new THREE.Scene 实例就好了。代码如下:
const scene = new THREE.Scene(); // 场景只有一种
复制代码
2、camera
const camera = new THREE.PerspectiveCamera(75,
window.innerWidth/window.innerHeight, 0.1, 1000)
复制代码
PerspectiveCamera( fov, aspect, near, far )
参数解析。
- fov(Number): 仰角的角度
- aspect(Number): 截平面长宽比,多为画布的长宽比。
- near(Number): 近面的距离
- far(Number): 远面的距离
3、renderer
渲染器决定了渲染的结果应该画在页面的什么元素上面,并且以怎样的方式来绘制。
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
复制代码
开始画地球
1.实例化一个 scence 对象, 用来存放我们的地球实体。
function initScene() {
scene = new THREE.Scene();
scene.opacity = 0;
scene.transparent = true;
}
复制代码
2.准备搭建 camera 的位置,和调节角度,在 three.js 里面采用给的是右手坐标系
const camera = new THREE.PerspectiveCamera(75,
window.innerWidth/window.innerHeight, 0.1, 1000)
复制代码
这里采用的是透视相机。视角越大,看到的场景越大,那么中间的物体相对于整个场景来说,就越小了。
3.渲染器 renderer
function initThree() {
// 实例化 THREE.WebGLRenderer 对象。
renderer = new THREE.WebGLRenderer({
antialias: true, // 抗锯齿
alpha: true, // 强度
canvas: renderer
});
// 设置 renderer 的大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 挂载到准备的 domElement 上
renderHelper(renderer.domElement)
// Sets the clear color and opacity.
renderer.setClearColor(0x000000, 1.0);
}
复制代码
4.画地球 ,这里的地球其实就是在一个球体上贴上带有地球纹路的贴纸
// 地球
function initEarth() {
// 实例化一个半径为 200 的球体
const earthGeo = new THREE.SphereGeometry(200, 100, 100);
const earthMater = new THREE.MeshPhongMaterial({
specular: 0x404040,
shininess: 5,
map: textureLoader.load('/earth.jpg'),
// 镜面反射
specularMap: textureLoader.load('/earth_spec.jpg'),
// 用于创建凹凸贴图的纹理
bumpMap: textureLoader.load('/earth_bump.jpg')
});
earthMesh = new THREE.Mesh(earthGeo, earthMater);
earthMesh.rotation.y = -correctRotate;
earthMesh.name = "earth"
scene.add(earthMesh);
}
复制代码
5.地球加上云层
const cloudsMesh;
function initClouds() {
// 实例化一个球体,半径要比地球的大一点,从而实现云飘咋地球上的感觉
var cloudsGeo = new THREE.SphereGeometry(201, 100, 100);
// transparent 与 opacity 搭配使用,设置材质的透明度,当 transparent 设为 true 时, 会对材质特殊处理,对性能会有些损耗。
var cloudsMater = new THREE.MeshPhongMaterial({
alphaMap: new THREE.TextureLoader().load('/clouds.jpg'),
transparent: true,
opacity: 0.2
});
cloudsMesh = new THREE.Mesh(cloudsGeo, cloudsMater);
scene.add(cloudsMesh);
}
复制代码
6.给世界来点光
function initLight() {
// 位于场景正上方的光源,颜色从天空颜色渐变为地面颜色。
// light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);
// 环境光
const allLight = new THREE.AmbientLight(0xFFFFFF);
allLight.position.set(100, 100, 200);
// 平行光 位置不同,方向光作用于物体的面也不同,看到的物体各个面的颜色也不一样
light = new THREE.DirectionalLight(0xffffbb, 1);
light.position.set(-11, 3, 1);
const sun = new THREE.SpotLight(0x393939, 2.5);
sun.position.set(-15, 10, 21);
scene.add(allLight, light, sun);
}
复制代码
7.坐标
/**
*lng:经度
*lat:维度
*radius:地球半径
*/
function Point(lng, lat, radius) {
// 将经纬度转换为three中的坐标
const lg = THREE.Math.degToRad(lng),
lt = THREE.Math.degToRad(lat);
const y = radius * Math.sin(lt);
const temp = radius * Math.cos(lt);
const x = temp * Math.sin(lg);
const z = temp * Math.cos(lg);
return {x: x, y: y, z: z}
}
function createPointMesh() {
// 打点
LOCATIONS.forEach(location => {
//设置坐标
const sprite = new THREE.Sprite(new THREE.SpriteMaterial(
{
map: textureLoader.load('/location.png'),
depthWrite: false, //禁止写入深度缓冲区数据
}));
const pos = Point(location.coord[1], location.coord[0], 200 * 1.05)
sprite.coord = {lg: location.coord[1], lt: location.coord[0]}
sprite.position.set(pos.x, pos.y, pos.z);
sprite.scale.set(20, 20, 1);
sprite.name = location.name
//城市名
const spriteText = new THREE.Sprite(new THREE.SpriteMaterial(
{
color: 0x000000,
map: textureLoader.load(`/i_${(location.name).toLowerCase()}.png`),
depthWrite: false, //禁止写入深度缓冲区数据
}));
spriteText.position.set(pos.x, pos.y, pos.z);
spriteText.scale.set(50, 50, 1.5);
locationGroup.add(sprite, spriteText);
scene.add(locationGroup);
}
)
}
复制代码
8.添加点击、旋转控制
function startControl() {
document.addEventListener('mousedown', onPointClick);
// 载入控制器
controls = new OrbitControls(camera, renderer.domElement);
}
function onPointClick(e) {
e.preventDefault();
// 鼠标点击位置的屏幕坐标转换成threejs中的标准坐标-1<x<1, -1<y<1
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
const intersects = raycaster.intersectObjects(locationGroup.children, true);
if (intersects.length > 0 && intersects[0].object.name) {
// 只取第一个相交物体
if (city) city.scale.set(20, 20, 1);
city = intersects[0].object;
rotateToCenter(city)
}
}
function rotateToCenter(city) {
// 放大
city.scale.set(30, 30, 1);
// 显示城市名
const cityName = city.name;
const cityText = document.querySelector(".showCity");
setTimeout(function () {
cityText.innerText = cityName;
}, 500)
// 旋转到中心
const rotateRad = rotate2Center(city.coord);
let finalY = -rotateRad.y;
while (earthMesh.rotation.y > 0 && finalY + Math.PI * 2 < earthMesh.rotation.y) finalY += Math.PI * 2;
while (earthMesh.rotation.y < 0 && finalY - Math.PI * 2 > earthMesh.rotation.y) finalY -= Math.PI * 2;
if (Math.abs(finalY - earthMesh.rotation.y) > Math.PI) {
if (finalY > earthMesh.rotation.y) finalY -= Math.PI * 2;
else finalY += Math.PI * 2;
}
const needRotateX = rotateRad.x - earthMesh.rotation.x + controls.object.rotation.x
const needRotateY = finalY - earthMesh.rotation.y - correctRotate + controls.object.rotation.y
rotateEarth(needRotateX, needRotateY);
}
// 转换为弧度
function rotate2Center(coord) {
return {x: degToRad(coord.lt), y: degToRad(coord.lg)};
}
// 转动操作
function rotateEarth(intervalX, intervalY) {
if (tween) tween.stop();
tween = new TWEEN.Tween({
rotateY: earthMesh.rotation.y,
rotateX: earthMesh.rotation.x,
rotateLoc: locationGroup.rotation.y
})
.to({
rotateY: earthMesh.rotation.y + intervalY,
rotateX: earthMesh.rotation.x + intervalX,
rotateLoc: locationGroup.rotation.y + intervalY
}, 1000);
tween.easing(TWEEN.Easing.Sinusoidal.InOut);
const onUpdate = function () {
earthMesh.rotation.y = this._object.rotateY;
earthMesh.rotation.x = this._object.rotateX;
locationGroup.rotation.y = this._object.rotateLoc;
locationGroup.rotation.x = this._object.rotateX;
}
const onComplete = function () {
}
tween.onUpdate(onUpdate);
tween.onComplete(onComplete);
tween.start();
}
function rotateToCity(cityName) {
city = locationGroup.children.find((location) => {
return location.name === cityName
})
rotateToCenter(city)
}
复制代码
9.动画与自转
function animate() {
stats.update();
renderer.clear();
earthMesh.rotate.y+=0.05
locationGroup.rotate.y+=0.05
renderer.render(scene, camera);
requestAnimationFrame(() => {
TWEEN.update();
if (controls) {
controls.update();
animate()
}
});
}
复制代码
完整代码
city.js
export const LOCATIONS = [{ name: 'SHANGHAI', coord: [30.40, 120.52] // 30° 40' N, 120° 52' E
}, {
name: 'BEIJING',
coord: [39.92, 116.46] // 39° 92' N, 116° 46' E
}, {
name: 'WASHINGTON',
coord: [38.91, -77.02] // 38° 91' N, 77° 02' W
}, {
name: 'MELBOURNE',
coord: [-37.50, 144.58] // 37° 50' S, 144° 58' E
}, {
name: 'RIO',
coord: [-22.54, -43.12] // 22° 54' S,43° 12' W
},
{
name: 'LONDON',
coord: [51.30, 0.5] // 22° 54' S,43° 12' W
},
{
name: 'ANTARCTICA',
coord: [-90,360]
},
{
name: 'GREENLAND',
coord: [75.930886,-40.253906]
},
{
name: 'NAMIBIA',
coord: [-22.57,17.086117]
}
]
复制代码
renderHelper.js
export const renderHelper = (dom) =>{
const el = document.querySelector('.showDemos')
if (!el)return
el.innerHTML = ''
el.appendChild(dom)
}
复制代码
earthScreen.js
import * as THREE from "three";
import {renderHelper} from "../renderHelper";
import Stats from "three/examples/jsm/libs/stats.module";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";
import {LOCATIONS} from "./city";
import {TWEEN} from "three/examples/jsm/libs/tween.module.min";
import {degToRad} from "three/src/math/MathUtils";
import {useEffect} from "react";
import './city.css'
export const EarthScreen = () => {
let scene, camera, renderer, earthMesh, light, controls, stats,
tween, city = null
const mouse = new THREE.Vector2();
const locationGroup = new THREE.Group();
const raycaster = new THREE.Raycaster()
const textureLoader = new THREE.TextureLoader();
const correctRotate = (Math.PI / 2).toFixed(2)
useEffect(() => {
threeStart();
rotateToCity('SHANGHAI')
}, [])
function initScene() {
scene = new THREE.Scene();
scene.opacity = 0;
scene.transparent = true;
}
function initCamera() {
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.z = 500;
}
// 渲染器
function initThree() {
// 实例化 THREE.WebGLRenderer 对象。
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
canvas: renderer
});
// 设置 renderer 的大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 挂载到准备的 domElement 上
renderHelper(renderer.domElement)
// Sets the clear color and opacity.
renderer.setClearColor(0x000000, 1.0);
}
// 帧蘋
function initStats() {
stats = new Stats();
const container = document.querySelector('.showDemos')
if (!container) return
container.appendChild(stats.dom)
}
// 地球
function initEarth() {
// 实例化一个半径为 200 的球体
const earthGeo = new THREE.SphereGeometry(200, 100, 100);
const earthMater = new THREE.MeshPhongMaterial({
specular: 0x404040,
shininess: 5,
map: textureLoader.load('/earth.jpg'),
specularMap: textureLoader.load('/earth_spec.jpg'),
bumpMap: textureLoader.load('/earth_bump.jpg')
});
earthMesh = new THREE.Mesh(earthGeo, earthMater);
earthMesh.rotation.y = -correctRotate;
earthMesh.name = "earth"
scene.add(earthMesh);
}
// 光源
function initLight() {
// 位于场景正上方的光源,颜色从天空颜色渐变为地面颜色。
// light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);
// 环境光
const allLight = new THREE.AmbientLight(0xFFFFFF);
allLight.position.set(100, 100, 200);
// 平行光 位置不同,方向光作用于物体的面也不同,看到的物体各个面的颜色也不一样
light = new THREE.DirectionalLight(0xffffbb, 1);
light.position.set(-11, 3, 1);
const sun = new THREE.SpotLight(0x393939, 2.5);
sun.position.set(-15, 10, 21);
scene.add(allLight, light, sun);
}
/**
*lng:经度
*lat:维度
*radius:地球半径
*/
function Point(lng, lat, radius) {
const lg = THREE.Math.degToRad(lng),
lt = THREE.Math.degToRad(lat);
const y = radius * Math.sin(lt);
const temp = radius * Math.cos(lt);
const x = temp * Math.sin(lg);
const z = temp * Math.cos(lg);
return {x: x, y: y, z: z}
}
function createPointMesh() {
// 打点
LOCATIONS.forEach(location => {
//设置坐标
const sprite = new THREE.Sprite(new THREE.SpriteMaterial(
{
map: textureLoader.load('/location.png'),
depthWrite: false, //禁止写入深度缓冲区数据
}));
const pos = Point(location.coord[1], location.coord[0], 200 * 1.05)
sprite.coord = {lg: location.coord[1], lt: location.coord[0]}
sprite.position.set(pos.x, pos.y, pos.z);
sprite.scale.set(20, 20, 1);
sprite.name = location.name
//城市名
const spriteText = new THREE.Sprite(new THREE.SpriteMaterial(
{
color: 0x000000,
map: textureLoader.load(`/i_${(location.name).toLowerCase()}.png`),
depthWrite: false, //禁止写入深度缓冲区数据
}));
spriteText.position.set(pos.x, pos.y, pos.z);
spriteText.scale.set(50, 50, 1.5);
locationGroup.add(sprite, spriteText);
scene.add(locationGroup);
}
)
}
function rotate2Center(coord) {
return {x: degToRad(coord.lt), y: degToRad(coord.lg)};
}
function rotateEarth(intervalX, intervalY) {
if (tween) tween.stop();
tween = new TWEEN.Tween({
rotateY: earthMesh.rotation.y,
rotateX: earthMesh.rotation.x,
rotateLoc: locationGroup.rotation.y
})
.to({
rotateY: earthMesh.rotation.y + intervalY,
rotateX: earthMesh.rotation.x + intervalX,
rotateLoc: locationGroup.rotation.y + intervalY
}, 1000);
tween.easing(TWEEN.Easing.Sinusoidal.InOut);
const onUpdate = function () {
earthMesh.rotation.y = this._object.rotateY;
earthMesh.rotation.x = this._object.rotateX;
locationGroup.rotation.y = this._object.rotateLoc;
locationGroup.rotation.x = this._object.rotateX;
}
const onComplete = function () {
}
tween.onUpdate(onUpdate);
tween.onComplete(onComplete);
tween.start();
}
function onPointClick(e) {
e.preventDefault();
// 鼠标点击位置的屏幕坐标转换成threejs中的标准坐标-1<x<1, -1<y<1
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
const intersects = raycaster.intersectObjects(locationGroup.children, true);
if (intersects.length > 0 && intersects[0].object.name) {
// 只取第一个相交物体
if (city) city.scale.set(20, 20, 1);
city = intersects[0].object;
rotateToCenter(city)
}
}
function rotateToCenter(city) {
// 放大
city.scale.set(30, 30, 1);
// 显示城市名
const cityName = city.name;
const cityText = document.querySelector(".showCity");
setTimeout(function () {
cityText.innerText = cityName;
}, 500)
// 旋转到中心
const rotateRad = rotate2Center(city.coord);
let finalY = -rotateRad.y;
while (earthMesh.rotation.y > 0 && finalY + Math.PI * 2 < earthMesh.rotation.y) finalY += Math.PI * 2;
while (earthMesh.rotation.y < 0 && finalY - Math.PI * 2 > earthMesh.rotation.y) finalY -= Math.PI * 2;
if (Math.abs(finalY - earthMesh.rotation.y) > Math.PI) {
if (finalY > earthMesh.rotation.y) finalY -= Math.PI * 2;
else finalY += Math.PI * 2;
}
const needRotateX = rotateRad.x - earthMesh.rotation.x + controls.object.rotation.x
const needRotateY = finalY - earthMesh.rotation.y - correctRotate + controls.object.rotation.y
rotateEarth(needRotateX, needRotateY);
}
function rotateToCity(cityName) {
city = locationGroup.children.find((location) => {
return location.name === cityName
})
rotateToCenter(city)
}
function startControl() {
document.addEventListener('mousedown', onPointClick);
// 载入控制器
controls = new OrbitControls(camera, renderer.domElement);
}
function threeStart() {
initThree();
initScene();
initCamera();
initStats();
initLight();
initEarth();
createPointMesh()
startControl()
animate();
}
function animate() {
stats.update();
renderer.clear();
renderer.render(scene, camera);
requestAnimationFrame(() => {
TWEEN.update();
if (controls) {
controls.update();
animate()
}
});
}
return <div className='showCity'/>
}
复制代码