目录
- 地形材质(MeshStandardMaterial)
- 飞线材质(ShaderMaterial)
- 侧边渐变材质(ShaderMaterial)
- 追光效果
- 飞线特效
- 旋转环装饰
- 高亮交互效果
- 呼吸浮动效果
一、地形材质(MeshStandardMaterial)
1.1 实现原理
使用 Three.js 的 PBR(物理正确渲染)材质,结合多张纹理实现真实感地形效果:
- 漫反射纹理(diffuseMap):控制表面颜色
- 位移纹理(displacementMap):控制地形起伏
- 法线纹理(normalMap):控制表面细节和光照反应
- 粗糙度纹理(roughnessMap):控制表面光滑程度
1.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>地形材质示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -50, 50);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
scene.add(directionalLight);
const textureLoader = new THREE.TextureLoader();
const textures = {
diffuseMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
displacementMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
normalMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
roughnessMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
};
textures.diffuseMap.wrapS = THREE.RepeatWrapping;
textures.diffuseMap.wrapT = THREE.RepeatWrapping;
textures.diffuseMap.repeat.set(2.8, 2.15);
const terrainMaterial = new THREE.MeshStandardMaterial({
color: '#0a1607',
emissive: '#101d08',
emissiveIntensity: 0.12,
map: textures.diffuseMap,
displacementMap: textures.displacementMap,
displacementScale: 6.5,
normalMap: textures.normalMap,
normalScale: new THREE.Vector2(1.0, 1.0),
roughnessMap: textures.roughnessMap,
roughness: 0.94,
metalness: 0.03,
transparent: true,
opacity: 0.85,
side: THREE.DoubleSide,
depthWrite: false,
});
const geometry = new THREE.SphereGeometry(20, 64, 64);
const terrain = new THREE.Mesh(geometry, terrainMaterial);
scene.add(terrain);
function animate() {
requestAnimationFrame(animate);
terrain.rotation.y += 0.002;
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
二、飞线材质(ShaderMaterial)
2.1 实现原理
使用自定义 ShaderMaterial 实现数据流向的动态飞线效果:
顶点着色器:传递 progress 属性到片元着色器
片元着色器:基于时间计算发光头位置,使用 smoothstep 创建三级渐变发光效果
- head:发光头位置(fract 循环)
- body:主体渐变
- core:核心高亮
2.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>飞线材质示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -50, 50);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const flyLineShader = {
uniforms: {
uTime: { value: 0 },
uDelay: { value: 0 },
uSpeed: { value: 0.22 },
uColor: { value: new THREE.Color('#F6FFD9') },
},
vertexShader: `
varying float vProgress;
attribute float progress;
void main() {
vProgress = progress;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform float uDelay;
uniform float uSpeed;
uniform vec3 uColor;
varying float vProgress;
void main() {
float head = fract((uTime + uDelay) * uSpeed - vProgress);
float body = smoothstep(0.24, 0.0, head);
float core = smoothstep(0.035, 0.0, head);
float alpha = body * 0.72 + core * 0.5;
gl_FragColor = vec4(uColor, alpha);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthTest: false,
depthWrite: false,
};
const curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(0, 0, 20),
new THREE.Vector3(25, -25, 40),
new THREE.Vector3(50, 0, 20)
);
const points = curve.getPoints(88);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const progress = new Float32Array(points.length);
for (let i = 0; i < points.length; i++) {
progress[i] = i / (points.length - 1);
}
geometry.setAttribute('progress', new THREE.BufferAttribute(progress, 1));
const material = new THREE.ShaderMaterial(flyLineShader);
const flyLine = new THREE.Line(geometry, material);
scene.add(flyLine);
function animate() {
requestAnimationFrame(animate);
material.uniforms.uTime.value += 0.016;
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
三、侧边渐变材质(ShaderMaterial)
3.1 实现原理
用于地图 3D 侧边墙的渐变渲染,根据顶点的 Z 坐标(深度)混合三种颜色:
- 底层:bottomColor → midColor(smoothstep 0~0.24)
- 上层:lower → topColor(smoothstep 0.34~1)
- 边缘发光:顶部边缘添加发光效果
3.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>侧边渐变材质示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -50, 50);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const sideGradientShader = {
uniforms: {
topColor: { value: new THREE.Color('#E8FF4F') },
midColor: { value: new THREE.Color('#a8bc38') },
bottomColor: { value: new THREE.Color('#101304') },
alpha: { value: 0.85 },
topZ: { value: 44 },
bottomZ: { value: 20 },
},
vertexShader: `
varying float vDepth;
uniform float topZ;
uniform float bottomZ;
void main() {
float depth = (position.z - bottomZ) / (topZ - bottomZ);
vDepth = clamp(depth, 0.0, 1.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 midColor;
uniform vec3 bottomColor;
uniform float alpha;
varying float vDepth;
void main() {
vec3 lower = mix(bottomColor, midColor, smoothstep(0.0, 0.24, vDepth));
vec3 color = mix(lower, topColor, smoothstep(0.34, 1.0, vDepth));
float edgeGlow = smoothstep(0.48, 1.0, vDepth);
color += edgeGlow * topColor * 0.24;
float finalAlpha = alpha * (0.46 + vDepth * 0.54);
gl_FragColor = vec4(color, finalAlpha);
}
`,
transparent: true,
side: THREE.DoubleSide,
};
const boxGeometry = new THREE.BoxGeometry(30, 30, 24);
const material = new THREE.ShaderMaterial(sideGradientShader);
const topFaceMaterial = new THREE.MeshStandardMaterial({
color: 0x2a2a3a,
roughness: 0.8,
metalness: 0.2
});
const materials = [
material, material,
topFaceMaterial, topFaceMaterial,
material, material
];
const box = new THREE.Mesh(boxGeometry, materials);
box.position.z = 32;
scene.add(box);
function animate() {
requestAnimationFrame(animate);
box.rotation.y += 0.005;
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
四、追光效果
4.1 实现原理
- 生成省份轮廓路径(网格化算法)
- 路径平滑处理(多次迭代)
- 构建多个 LineBasicMaterial 线段,透明度沿路径渐变
- 动画循环中更新每个线段的起点和终点位置,形成追逐效果
4.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>追光效果示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -60, 60);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const chaseLoop = [];
const numPoints = 60;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const radius = 20 + Math.sin(angle * 3) * 5;
chaseLoop.push(new THREE.Vector3(
Math.cos(angle) * radius,
Math.sin(angle) * radius,
25
));
}
const distances = [];
let totalDistance = 0;
for (let i = 0; i < chaseLoop.length; i++) {
const p1 = chaseLoop[i];
const p2 = chaseLoop[(i + 1) % chaseLoop.length];
totalDistance += p1.distanceTo(p2);
distances.push(totalDistance);
}
const segmentCount = 34;
const segments = [];
const chaseGroup = new THREE.Group();
for (let i = 0; i < segmentCount; i++) {
const fade = 1 - i / segmentCount;
const positions = new Float32Array(6);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.LineBasicMaterial({
color: '#ffffff',
transparent: true,
opacity: Math.pow(fade, 1.35) * 0.9,
blending: THREE.AdditiveBlending,
depthTest: false,
depthWrite: false,
});
const line = new THREE.Line(geometry, material);
line.frustumCulled = false;
chaseGroup.add(line);
segments.push({ geometry, material, positions });
}
scene.add(chaseGroup);
function samplePoint(distance) {
while (distance < 0) distance += totalDistance;
while (distance > totalDistance) distance -= totalDistance;
for (let i = 0; i < distances.length; i++) {
if (distances[i] >= distance) {
const prevDist = i === 0 ? 0 : distances[i - 1];
const t = (distance - prevDist) / (distances[i] - prevDist);
const p1 = chaseLoop[i];
const p2 = chaseLoop[(i + 1) % chaseLoop.length];
return p1.clone().lerp(p2, t);
}
}
return chaseLoop[0].clone();
}
let time = 0;
const chaseSpeed = 320;
const tailLength = Math.max(90, totalDistance * 0.06);
function animate() {
requestAnimationFrame(animate);
time += 0.016;
const headDistance = (time * chaseSpeed) % totalDistance;
const segmentLength = tailLength / segmentCount;
segments.forEach((segment, index) => {
const start = samplePoint(headDistance - index * segmentLength);
const end = samplePoint(headDistance - (index + 0.88) * segmentLength);
segment.positions[0] = start.x;
segment.positions[1] = start.y;
segment.positions[2] = start.z;
segment.positions[3] = end.x;
segment.positions[4] = end.y;
segment.positions[5] = end.z;
const fade = 1 - index / segments.length;
segment.material.opacity = Math.pow(fade, 1.35) * 0.9;
const positionAttribute = segment.geometry.getAttribute('position');
positionAttribute.needsUpdate = true;
});
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
五、飞线特效
5.1 实现原理
- 确定源点(首都/省会)和目标点(其他城市)
- 使用 QuadraticBezierCurve3 创建贝塞尔曲线路径
- 双层结构:基线(静态)+ 流动线(动态)
- 流动线使用 ShaderMaterial 实现发光流动效果
5.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>飞线特效示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -80, 80);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const sourcePoint = new THREE.Vector3(0, 0, 20);
const targets = [
{ name: '杭州', pos: new THREE.Vector3(30, -20, 20) },
{ name: '宁波', pos: new THREE.Vector3(45, -10, 20) },
{ name: '温州', pos: new THREE.Vector3(20, -40, 20) },
{ name: '嘉兴', pos: new THREE.Vector3(35, 5, 20) },
{ name: '湖州', pos: new THREE.Vector3(30, 15, 20) },
];
const flyLineShader = {
uniforms: {
uTime: { value: 0 },
uDelay: { value: 0 },
uSpeed: { value: 0.3 },
uColor: { value: new THREE.Color('#F6FFD9') },
},
vertexShader: `
varying float vProgress;
attribute float progress;
void main() {
vProgress = progress;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform float uDelay;
uniform float uSpeed;
uniform vec3 uColor;
varying float vProgress;
void main() {
float head = fract((uTime + uDelay) * uSpeed - vProgress);
float body = smoothstep(0.24, 0.0, head);
float core = smoothstep(0.035, 0.0, head);
float alpha = body * 0.72 + core * 0.5;
gl_FragColor = vec4(uColor, alpha);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthTest: false,
depthWrite: false,
};
targets.forEach((target, index) => {
const midPoint = sourcePoint.clone().add(target.pos).multiplyScalar(0.5);
midPoint.z = Math.max(28, sourcePoint.distanceTo(target.pos) * 0.15);
const curve = new THREE.QuadraticBezierCurve3(
sourcePoint.clone(),
midPoint,
target.pos.clone()
);
const points = curve.getPoints(88);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const progress = new Float32Array(points.length);
for (let i = 0; i < points.length; i++) {
progress[i] = i / (points.length - 1);
}
geometry.setAttribute('progress', new THREE.BufferAttribute(progress, 1));
const material = new THREE.ShaderMaterial({
...flyLineShader,
uniforms: {
...flyLineShader.uniforms,
uDelay: { value: index * 0.5 },
},
});
const flyLine = new THREE.Line(geometry, material);
scene.add(flyLine);
const baseLineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const baseLineMaterial = new THREE.LineBasicMaterial({
color: 0xE8FF4F,
transparent: true,
opacity: 0.15,
});
const baseLine = new THREE.Line(baseLineGeometry, baseLineMaterial);
scene.add(baseLine);
});
const sourceMarker = new THREE.Mesh(
new THREE.SphereGeometry(2, 16, 16),
new THREE.MeshBasicMaterial({ color: 0xE8FF4F })
);
sourceMarker.position.copy(sourcePoint);
scene.add(sourceMarker);
function animate() {
requestAnimationFrame(animate);
flyLineShader.uniforms.uTime.value += 0.016;
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
六、旋转环装饰
6.1 实现原理
多层环形装饰元素,包含:
- 软边缘环(RingMesh)
- 弧线组(多个圆弧)
- 刻度线(均匀分布的短线)
- 整体旋转动画
6.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>旋转环装饰示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -100, 100);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const ringGroup = new THREE.Group();
const softRingGeometry = new THREE.RingGeometry(356, 362, 192);
const softRingMaterial = new THREE.MeshBasicMaterial({
color: 0xE8FF4F,
transparent: true,
opacity: 0.055,
side: THREE.DoubleSide,
});
const softRing = new THREE.Mesh(softRingGeometry, softRingMaterial);
softRing.rotation.x = -Math.PI / 2;
ringGroup.add(softRing);
const innerSoftRingGeometry = new THREE.RingGeometry(244, 248, 160);
const innerSoftRingMaterial = new THREE.MeshBasicMaterial({
color: 0xE8FF4F,
transparent: true,
opacity: 0.035,
side: THREE.DoubleSide,
});
const innerSoftRing = new THREE.Mesh(innerSoftRingGeometry, innerSoftRingMaterial);
innerSoftRing.rotation.x = -Math.PI / 2;
ringGroup.add(innerSoftRing);
const arcRadii = [390, 316, 252];
const arcMaterial = new THREE.LineBasicMaterial({
color: 0xE8FF4F,
transparent: true,
opacity: 0.24,
});
const dimArcMaterial = new THREE.LineBasicMaterial({
color: 0xb5ca40,
transparent: true,
opacity: 0.15,
});
arcRadii.forEach((radius) => {
for (let i = 0; i < 11; i++) {
const startAngle = (i / 11) * Math.PI * 2;
const endAngle = startAngle + Math.PI / 6;
const arcCurve = new THREE.ArcCurve(0, 0, radius, startAngle, endAngle, false);
const arcPoints = arcCurve.getPoints(32);
const arcGeometry = new THREE.BufferGeometry().setFromPoints(arcPoints);
const arcLine = new THREE.Line(arcGeometry, i % 2 === 0 ? arcMaterial : dimArcMaterial);
arcLine.rotation.x = -Math.PI / 2;
ringGroup.add(arcLine);
}
});
const tickMaterial = new THREE.LineBasicMaterial({
color: 0xE8FF4F,
transparent: true,
opacity: 0.18,
});
for (let i = 0; i < 48; i++) {
if (i % 4 === 0) continue;
const angle = (i / 48) * Math.PI * 2;
const innerRadius = 344;
const outerRadius = 370;
const startPoint = new THREE.Vector3(
Math.cos(angle) * innerRadius,
Math.sin(angle) * innerRadius,
0
);
const endPoint = new THREE.Vector3(
Math.cos(angle) * outerRadius,
Math.sin(angle) * outerRadius,
0
);
const tickGeometry = new THREE.BufferGeometry().setFromPoints([startPoint, endPoint]);
const tickLine = new THREE.Line(tickGeometry, tickMaterial);
tickLine.rotation.x = -Math.PI / 2;
ringGroup.add(tickLine);
}
scene.add(ringGroup);
function animate() {
requestAnimationFrame(animate);
ringGroup.rotation.z += 0.004;
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
七、高亮交互效果
7.1 实现原理
鼠标悬停时触发以下效果:
- 区域抬升(平滑过渡到目标高度)
- 侧边墙变色(颜色变亮)
- 高亮层透明度增加
- 地形材质变化(颜色、自发光、透明度)
所有状态变化均使用 lerp 插值实现平滑过渡。
7.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高亮交互效果示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -50, 50);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
scene.add(directionalLight);
const group = new THREE.Group();
const baseMaterial = new THREE.MeshStandardMaterial({
color: '#07100b',
roughness: 0.94,
metalness: 0.03,
side: THREE.DoubleSide,
});
const highlightMaterial = new THREE.MeshBasicMaterial({
color: 0xE8FF4F,
transparent: true,
opacity: 0,
blending: THREE.AdditiveBlending,
depthTest: false,
depthWrite: false,
});
const geometries = [];
const colors = ['#07100b', '#0a1607', '#0d1c09'];
for (let i = 0; i < 5; i++) {
const geometry = new THREE.BoxGeometry(15, 15, 24);
geometries.push(geometry);
const material = baseMaterial.clone();
material.color.set(colors[i % colors.length]);
const box = new THREE.Mesh(geometry, material);
box.position.set((i - 2) * 20, 0, 32);
box.userData = {
originalZ: 32,
targetZ: 32,
originalColor: material.color.clone(),
isHighlighted: false,
};
group.add(box);
const highlightBox = new THREE.Mesh(geometry, highlightMaterial.clone());
highlightBox.position.copy(box.position);
highlightBox.scale.set(1.02, 1.02, 1.02);
highlightBox.userData = { isHighlight: true };
group.add(highlightBox);
}
scene.add(group);
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
container.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / container.clientWidth) * 2 - 1;
mouse.y = -(event.clientY / container.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(group.children);
group.children.forEach((child) => {
if (child.userData.isHighlight) return;
const isHighlighted = intersects.some((intersect) => intersect.object === child);
child.userData.isHighlighted = isHighlighted;
if (isHighlighted) {
child.userData.targetZ = 48;
child.material.emissive.set(0xE8FF4F);
child.material.emissiveIntensity = 0.72;
} else {
child.userData.targetZ = 32;
child.material.emissive.set(0x000000);
child.material.emissiveIntensity = 0;
}
});
group.children.forEach((child) => {
if (!child.userData.isHighlight) return;
const targetBox = group.children.find((c) =>
Math.abs(c.position.x - child.position.x) < 1 &&
!c.userData.isHighlight
);
if (targetBox?.userData.isHighlighted) {
child.material.opacity = 0.32;
} else {
child.material.opacity = 0;
}
});
});
function animate() {
requestAnimationFrame(animate);
group.children.forEach((child) => {
if (child.userData.isHighlight) return;
const lerpFactor = 0.18;
child.position.z += (child.userData.targetZ - child.position.z) * lerpFactor;
child.material.opacity += ((child.userData.isHighlighted ? 0.68 : 1) - child.material.opacity) * lerpFactor;
group.children.forEach((highlightChild) => {
if (!highlightChild.userData.isHighlight) return;
if (Math.abs(highlightChild.position.x - child.position.x) < 1) {
highlightChild.position.z = child.position.z;
}
});
});
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
八、呼吸浮动效果
8.1 实现原理
地图整体沿 Z 轴做正弦波动动画,产生呼吸般的浮动效果:
typescript
mapGroup.position.z = basePosition.z + Math.sin(t * 0.55) * 2;
周期约 11.4 秒,振幅 2 单位。
8.2 完整 HTML 实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>呼吸浮动效果示例</title>
<style>
body { margin: 0; background: #0a0a0f; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, -60, 60);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
scene.add(directionalLight);
const mapGroup = new THREE.Group();
const geometry = new THREE.BoxGeometry(40, 30, 24);
const material = new THREE.MeshStandardMaterial({
color: '#07100b',
roughness: 0.94,
metalness: 0.03,
side: THREE.DoubleSide,
});
const box = new THREE.Mesh(geometry, material);
box.position.z = 32;
mapGroup.add(box);
const sideGeometry = new THREE.BoxGeometry(40, 30, 24);
const sideMaterial = new THREE.MeshBasicMaterial({
color: 0xE8FF4F,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
});
const sideBox = new THREE.Mesh(sideGeometry, sideMaterial);
sideBox.position.z = 32;
sideBox.scale.set(1.05, 1.05, 1.05);
mapGroup.add(sideBox);
scene.add(mapGroup);
const basePosition = mapGroup.position.clone();
const floatAmplitude = 2;
const floatFrequency = 0.55;
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.016;
mapGroup.position.z = basePosition.z + Math.sin(time * floatFrequency) * floatAmplitude;
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
九、总结
| 特效/材质 | 类型 | 核心技术 |
|---|---|---|
| 地形材质 | MeshStandardMaterial | PBR 渲染 + 多纹理 |
| 飞线材质 | ShaderMaterial | 自定义着色器 + 时间动画 |
| 侧边渐变材质 | ShaderMaterial | 深度渐变 + smoothstep |
| 追光效果 | LineBasicMaterial | 路径采样 + 逐段更新 |
| 飞线特效 | ShaderMaterial + Line | 贝塞尔曲线 + 动态发光 |
| 旋转环装饰 | RingMesh + Line | 多层嵌套 + 旋转动画 |
| 高亮交互效果 | MeshStandardMaterial | Raycaster + lerp 插值 |
| 呼吸浮动效果 | Group | 正弦波动 |
所有特效均使用 Three.js 原生 API 实现,无需额外依赖,可直接复制到项目中使用。