Skip to content

Commit 36d98c0

Browse files
committed
chore: release v2.24.1
- fix: 修复气泡图 (PointLayer fill) 默认动画参数开启导致动画异常的 bug - feat: 升级 @antv/l7-three 集成,优化 ThreeRenderService 和 BaseLayer - docs: 补充 L7-Three 相关文档说明 - examples: 新增 three 相关示例 (animation/buildings/earthquake/particles/shader)
1 parent a54824d commit 36d98c0

28 files changed

Lines changed: 1153 additions & 35 deletions

examples/demos/extend/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
export { exportImage } from './export-image';
2+
export { threeAnimation } from './three-animation';
3+
export { threeBuildings } from './three-buildings';
4+
export { threeEarthquake } from './three-earthquake';
25
export { threeGeometry } from './three-geometry';
6+
export { threeParticles } from './three-particles';
7+
export { threeShader } from './three-shader';
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { ThreeLayer, ThreeRender } from '@antv/l7-three';
2+
import * as THREE from 'three';
3+
import type { TestCase } from '../../types';
4+
import { CaseScene } from '../../utils';
5+
6+
/**
7+
* Three.js 动画效果演示
8+
* 展示动态旋转、缩放和波动的 3D 对象
9+
*/
10+
export const threeAnimation: TestCase = async (options) => {
11+
const scene = await CaseScene({
12+
...options,
13+
// Three.js r163+ 需要 WebGL2,必须使用 'device' 渲染器
14+
renderer: 'device',
15+
mapConfig: {
16+
style: 'dark',
17+
center: [116.4074, 39.9042],
18+
zoom: 12,
19+
pitch: 45,
20+
rotation: 0,
21+
},
22+
});
23+
24+
scene.registerRenderService(ThreeRender);
25+
26+
const center = scene.getCenter();
27+
const animatedObjects: Array<{
28+
mesh: THREE.Mesh;
29+
type: 'rotate' | 'pulse' | 'wave';
30+
speed: number;
31+
initialScale: number;
32+
}> = [];
33+
34+
const threeJSLayer = new ThreeLayer({
35+
enableMultiPassRenderer: false,
36+
onAddMeshes: (threeScene, layer) => {
37+
// 环境光照
38+
threeScene.add(new THREE.AmbientLight(0xffffff, 0.4));
39+
40+
// 主光源
41+
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8);
42+
mainLight.position.set(50, 100, 50);
43+
threeScene.add(mainLight);
44+
45+
// 彩色点光源
46+
const colors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff];
47+
colors.forEach((color, i) => {
48+
const angle = (i / colors.length) * Math.PI * 2;
49+
const radius = 0.05;
50+
const light = new THREE.PointLight(color, 1, 20000);
51+
layer.setObjectLngLat(
52+
light,
53+
[center.lng + Math.cos(angle) * radius, center.lat + Math.sin(angle) * radius],
54+
5000,
55+
);
56+
threeScene.add(light);
57+
});
58+
59+
// 创建旋转的环
60+
for (let i = 0; i < 3; i++) {
61+
const radius = 2000 + i * 1500;
62+
const tube = 200;
63+
const geometry = new THREE.TorusGeometry(radius, tube, 16, 100);
64+
const material = new THREE.MeshPhongMaterial({
65+
color: [0x049ef4, 0x00ff88, 0xff6600][i],
66+
emissive: [0x049ef4, 0x00ff88, 0xff6600][i],
67+
emissiveIntensity: 0.3,
68+
transparent: true,
69+
opacity: 0.8,
70+
});
71+
const torus = new THREE.Mesh(geometry, material);
72+
73+
// 不同的初始旋转
74+
torus.rotation.x = Math.PI / 2 + i * 0.3;
75+
torus.rotation.y = i * 0.5;
76+
77+
layer.setObjectLngLat(torus, [center.lng, center.lat], 0);
78+
threeScene.add(torus);
79+
80+
animatedObjects.push({
81+
mesh: torus,
82+
type: 'rotate',
83+
speed: 0.01 * (i + 1) * (i % 2 === 0 ? 1 : -1),
84+
initialScale: 1,
85+
});
86+
}
87+
88+
// 创建脉冲球体
89+
for (let i = 0; i < 5; i++) {
90+
const angle = (i / 5) * Math.PI * 2;
91+
const distance = 0.03;
92+
const geometry = new THREE.SphereGeometry(500, 32, 32);
93+
const material = new THREE.MeshPhongMaterial({
94+
color: 0xff3366,
95+
emissive: 0xff3366,
96+
emissiveIntensity: 0.4,
97+
transparent: true,
98+
opacity: 0.7,
99+
});
100+
const sphere = new THREE.Mesh(geometry, material);
101+
102+
layer.setObjectLngLat(
103+
sphere,
104+
[center.lng + Math.cos(angle) * distance, center.lat + Math.sin(angle) * distance],
105+
1000,
106+
);
107+
threeScene.add(sphere);
108+
109+
animatedObjects.push({
110+
mesh: sphere,
111+
type: 'pulse',
112+
speed: 0.02 + i * 0.005,
113+
initialScale: 1,
114+
});
115+
}
116+
117+
// 创建波动效果的地表
118+
const planeGeometry = new THREE.PlaneGeometry(20000, 20000, 32, 32);
119+
const planeMaterial = new THREE.MeshPhongMaterial({
120+
color: 0x049ef4,
121+
emissive: 0x0044aa,
122+
emissiveIntensity: 0.2,
123+
transparent: true,
124+
opacity: 0.5,
125+
wireframe: true,
126+
side: THREE.DoubleSide,
127+
});
128+
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
129+
plane.rotation.x = -Math.PI / 2;
130+
131+
// 保存原始顶点位置用于动画
132+
const positions = planeGeometry.attributes.position.array as Float32Array;
133+
plane.userData.originalPositions = new Float32Array(positions);
134+
135+
layer.setObjectLngLat(plane, [center.lng, center.lat], -500);
136+
threeScene.add(plane);
137+
138+
animatedObjects.push({
139+
mesh: plane,
140+
type: 'wave',
141+
speed: 0.02,
142+
initialScale: 1,
143+
});
144+
145+
// 创建中心发光核心
146+
const coreGeometry = new THREE.IcosahedronGeometry(800, 2);
147+
const coreMaterial = new THREE.MeshPhongMaterial({
148+
color: 0xffffff,
149+
emissive: 0x049ef4,
150+
emissiveIntensity: 0.8,
151+
flatShading: true,
152+
});
153+
const core = new THREE.Mesh(coreGeometry, coreMaterial);
154+
layer.setObjectLngLat(core, [center.lng, center.lat], 3000);
155+
threeScene.add(core);
156+
157+
animatedObjects.push({
158+
mesh: core,
159+
type: 'rotate',
160+
speed: 0.03,
161+
initialScale: 1,
162+
});
163+
164+
// 动画更新
165+
const clock = new THREE.Clock();
166+
167+
const animate = () => {
168+
const time = clock.getElapsedTime();
169+
170+
animatedObjects.forEach((obj) => {
171+
if (obj.type === 'rotate') {
172+
obj.mesh.rotation.z += obj.speed;
173+
obj.mesh.rotation.x += obj.speed * 0.5;
174+
} else if (obj.type === 'pulse') {
175+
const scale = obj.initialScale + Math.sin(time * 3 + obj.speed * 100) * 0.3;
176+
obj.mesh.scale.set(scale, scale, scale);
177+
} else if (obj.type === 'wave') {
178+
const positions = (obj.mesh.geometry as THREE.PlaneGeometry).attributes.position
179+
.array as Float32Array;
180+
const originals = obj.mesh.userData.originalPositions as Float32Array;
181+
182+
for (let i = 0; i < positions.length; i += 3) {
183+
const x = originals[i];
184+
const y = originals[i + 1];
185+
positions[i + 2] =
186+
Math.sin(x * 0.001 + time) * 500 + Math.cos(y * 0.001 + time) * 500;
187+
}
188+
189+
(obj.mesh.geometry as THREE.PlaneGeometry).attributes.position.needsUpdate = true;
190+
obj.mesh.geometry.computeVertexNormals();
191+
}
192+
});
193+
194+
requestAnimationFrame(animate);
195+
};
196+
197+
animate();
198+
},
199+
}).animate(true);
200+
201+
scene.addLayer(threeJSLayer);
202+
203+
return scene;
204+
};
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { ThreeLayer, ThreeRender } from '@antv/l7-three';
2+
import * as THREE from 'three';
3+
import type { TestCase } from '../../types';
4+
import { CaseScene } from '../../utils';
5+
6+
/**
7+
* Three.js 3D 建筑物效果
8+
* 使用多边形数据创建立体建筑物
9+
*/
10+
export const threeBuildings: TestCase = async (options) => {
11+
const scene = await CaseScene({
12+
...options,
13+
// Three.js r163+ 需要 WebGL2,必须使用 'device' 渲染器
14+
renderer: 'device',
15+
mapConfig: {
16+
style: 'dark',
17+
// 数据位于深圳区域
18+
center: [113.95, 22.535],
19+
zoom: 12,
20+
pitch: 60,
21+
rotation: 0,
22+
},
23+
});
24+
25+
scene.registerRenderService(ThreeRender);
26+
27+
// 加载建筑物数据
28+
const data = await fetch(
29+
'https://gw.alipayobjects.com/os/basement_prod/972566c5-a2b9-4a7e-8da1-bae9d0eb0117.json',
30+
).then((res) => res.json());
31+
32+
const threeJSLayer = new ThreeLayer({
33+
enableMultiPassRenderer: false,
34+
onAddMeshes: (threeScene, layer) => {
35+
// 环境光照
36+
threeScene.add(new THREE.AmbientLight(0xffffff, 0.3));
37+
38+
// 方向光(模拟阳光)
39+
const sunLight = new THREE.DirectionalLight(0xffffff, 0.8);
40+
sunLight.position.set(100, 200, 100);
41+
sunLight.castShadow = true;
42+
threeScene.add(sunLight);
43+
44+
// 蓝色补光
45+
const fillLight = new THREE.DirectionalLight(0x049ef4, 0.3);
46+
fillLight.position.set(-100, 50, -100);
47+
threeScene.add(fillLight);
48+
49+
// 处理建筑物数据
50+
const features = data.features || [];
51+
52+
features.forEach((feature: any) => {
53+
// h20 是活动密度值(0~12),applyObjectLngLat 的 altitude 单位为米
54+
// rawValue * 50 → 高度范围 50~600m,与城市建筑物尺度匹配
55+
const rawValue = feature.properties?.h20 || 1;
56+
const height = Math.max(rawValue * 10, 10);
57+
const geometryType = feature.geometry?.type;
58+
// 兼容 Polygon 和 MultiPolygon
59+
const polygons: number[][][][] =
60+
geometryType === 'MultiPolygon'
61+
? feature.geometry.coordinates
62+
: [feature.geometry.coordinates];
63+
64+
polygons.forEach((polygonCoords: number[][][]) => {
65+
const outerRing = polygonCoords[0];
66+
if (!outerRing || outerRing.length < 3) return;
67+
68+
const color = getBuildingColor(rawValue);
69+
const buildingMesh = createBuildingBox(outerRing, height, color);
70+
if (!buildingMesh) return;
71+
72+
const center = getRingCenter(outerRing);
73+
if (!center) return;
74+
75+
// applyObjectLngLat 正确处理默认地图和高德地图的坐标换算
76+
// altitude = height/2 使建筑物底面贴地,顶面在 height 处
77+
layer.applyObjectLngLat(buildingMesh, center, height / 2);
78+
threeScene.add(buildingMesh);
79+
});
80+
});
81+
},
82+
}).animate(false);
83+
84+
scene.addLayer(threeJSLayer);
85+
86+
// 地图交互时触发重绘,避免持续动画循环导致卡顿
87+
scene.on('mapMove', () => scene.render());
88+
scene.on('zoomChange', () => scene.render());
89+
scene.on('rotateChange', () => scene.render());
90+
scene.on('pitchChange', () => scene.render());
91+
92+
return scene;
93+
};
94+
95+
/**
96+
* 从多边形外环顶点创建立柱(Mesh)
97+
* - outerRing: 经纬度坐标点数组(单个环)
98+
* - height: 柱子高度(米),将通过 applyObjectLngLat 的 altitude 参数正确映射
99+
*/
100+
function createBuildingBox(
101+
outerRing: number[][],
102+
height: number,
103+
color: number,
104+
): THREE.Mesh | null {
105+
if (!outerRing || outerRing.length < 3) return null;
106+
107+
// 计算多边形边界框
108+
let minX = Infinity,
109+
maxX = -Infinity;
110+
let minY = Infinity,
111+
maxY = -Infinity;
112+
outerRing.forEach(([x, y]) => {
113+
minX = Math.min(minX, x);
114+
maxX = Math.max(maxX, x);
115+
minY = Math.min(minY, y);
116+
maxY = Math.max(maxY, y);
117+
});
118+
119+
// 将经纬度差值粗略转换为米(applyObjectLngLat 的本地坐标系单位是米)
120+
const width = Math.max((maxX - minX) * 100000, 20);
121+
const depth = Math.max((maxY - minY) * 100000, 20);
122+
123+
// BoxGeometry(width, height_local, depth):Three.js 默认 Y 轴为高度方向
124+
const geometry = new THREE.BoxGeometry(width, height, depth);
125+
const material = new THREE.MeshPhongMaterial({ color, flatShading: false });
126+
const mesh = new THREE.Mesh(geometry, material);
127+
128+
// 绕 X 轴旋转 90°:将 Three.js 本地 Y 轴(高度)映射到地图的 Z 轴(altitude 方向)
129+
// 与 CylinderGeometry 的处理方式一致
130+
mesh.rotation.x = Math.PI / 2;
131+
132+
return mesh;
133+
}
134+
135+
/**
136+
* 根据活动密度值返回颜色
137+
*/
138+
function getBuildingColor(value: number): number {
139+
if (value >= 10) return 0xff3366;
140+
if (value >= 8) return 0xff6633;
141+
if (value >= 6) return 0xffcc33;
142+
if (value >= 4) return 0x33cc66;
143+
if (value >= 2) return 0x3366ff;
144+
return 0x444466;
145+
}
146+
147+
/**
148+
* 计算外环中心点
149+
*/
150+
function getRingCenter(outerRing: number[][]): [number, number] | null {
151+
if (!outerRing || outerRing.length === 0) return null;
152+
let sumX = 0,
153+
sumY = 0;
154+
outerRing.forEach(([x, y]) => {
155+
sumX += x;
156+
sumY += y;
157+
});
158+
return [sumX / outerRing.length, sumY / outerRing.length];
159+
}

0 commit comments

Comments
 (0)