Compare commits
5 Commits
v0.51.0
...
feature/ue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c97dd83f3e | ||
|
|
a4e6ce7489 | ||
|
|
7ffc8537e4 | ||
|
|
4dd396ea65 | ||
|
|
1e6f4b338b |
@@ -15,6 +15,7 @@ from app.api.v1 import (
|
||||
bgp,
|
||||
system_control,
|
||||
tv,
|
||||
ue_data,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -35,3 +36,4 @@ api_router.include_router(system_control.router, prefix="/system", tags=["system
|
||||
api_router.include_router(visualization.router, prefix="/visualization", tags=["visualization"])
|
||||
api_router.include_router(bgp.router, prefix="/bgp", tags=["bgp"])
|
||||
api_router.include_router(tv.router, prefix="/tv", tags=["tv"])
|
||||
api_router.include_router(ue_data.router, prefix="/ue", tags=["ue-client"])
|
||||
|
||||
351
backend/app/api/v1/ue_data.py
Normal file
351
backend/app/api/v1/ue_data.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""UE Client Data API
|
||||
|
||||
Flat JSON endpoints designed for easy parsing in Unreal Engine C++/Blueprint.
|
||||
Avoids GeoJSON nesting — every field is at the top level of each item.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
|
||||
from app.core.collected_data_fields import get_record_field
|
||||
from app.core.satellite_tle import build_tle_lines_from_elements
|
||||
from app.core.time import to_iso8601_utc
|
||||
from app.db.session import get_db
|
||||
from app.models.collected_data import CollectedData
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _current_stmt(source: str, limit: Optional[int] = None):
|
||||
stmt = (
|
||||
select(CollectedData)
|
||||
.where(CollectedData.source == source)
|
||||
.where(CollectedData.is_current.is_(True))
|
||||
.order_by(CollectedData.id.desc())
|
||||
)
|
||||
if limit:
|
||||
stmt = stmt.limit(limit)
|
||||
return stmt
|
||||
|
||||
|
||||
async def _fetch(db: AsyncSession, source: str, limit: Optional[int] = None) -> List[CollectedData]:
|
||||
result = await db.execute(_current_stmt(source, limit))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> Optional[float]:
|
||||
try:
|
||||
v = float(value)
|
||||
return v if v == v else None # reject NaN
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/status")
|
||||
async def ue_status(db: AsyncSession = Depends(get_db)):
|
||||
"""Quick health-check + data counts for the UE client."""
|
||||
from datetime import UTC, datetime
|
||||
|
||||
async def count_source(source: str) -> int:
|
||||
result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(CollectedData)
|
||||
.where(CollectedData.source == source)
|
||||
.where(CollectedData.is_current.is_(True))
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"server_time": to_iso8601_utc(datetime.now(UTC)),
|
||||
"compute_points_count": await count_source("top500"),
|
||||
"cables_count": await count_source("telegeography_cables"),
|
||||
"landing_points_count": await count_source("arcgis_landing"),
|
||||
"satellites_count": await count_source("celestrak"),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compute points (TOP500 supercomputers)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/compute-points")
|
||||
async def ue_compute_points(
|
||||
limit: int = Query(default=500, ge=1, le=2000),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Returns TOP500 supercomputer data as a flat JSON array.
|
||||
|
||||
Response shape:
|
||||
{
|
||||
"count": 500,
|
||||
"items": [
|
||||
{
|
||||
"id": "top500_1",
|
||||
"name": "Frontier",
|
||||
"latitude": 36.01,
|
||||
"longitude": -84.26,
|
||||
"country": "United States",
|
||||
"city": "Oak Ridge",
|
||||
"rank": 1,
|
||||
"rmax_tflops": 1194000.0,
|
||||
"rpeak_tflops": 1679616.0,
|
||||
"cores": 8730112,
|
||||
"power_kw": 22703.0
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
records = await _fetch(db, "top500", limit)
|
||||
items = []
|
||||
for record in records:
|
||||
meta = record.extra_data or {}
|
||||
lat = _safe_float(get_record_field(record, "latitude"))
|
||||
lon = _safe_float(get_record_field(record, "longitude"))
|
||||
if lat is None or lon is None:
|
||||
continue
|
||||
items.append({
|
||||
"id": f"top500_{record.id}",
|
||||
"name": record.name or "Unknown",
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"country": get_record_field(record, "country") or "",
|
||||
"city": get_record_field(record, "city") or "",
|
||||
"rank": meta.get("rank"),
|
||||
"rmax_tflops": _safe_float(get_record_field(record, "rmax")),
|
||||
"rpeak_tflops": _safe_float(get_record_field(record, "rpeak")),
|
||||
"cores": meta.get("cores"),
|
||||
"power_kw": _safe_float(get_record_field(record, "power")),
|
||||
})
|
||||
return {"count": len(items), "items": items}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cable landing points
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/landing-points")
|
||||
async def ue_landing_points(db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Returns cable landing points as a flat JSON array.
|
||||
|
||||
Response shape:
|
||||
{
|
||||
"count": 1200,
|
||||
"items": [
|
||||
{
|
||||
"id": "lp_42",
|
||||
"name": "Shoreham",
|
||||
"latitude": 50.83,
|
||||
"longitude": -0.28,
|
||||
"country": "United Kingdom",
|
||||
"city": "Shoreham-by-Sea",
|
||||
"cable_names": ["FLAG", "TAT-14"]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
# Load landing points
|
||||
lp_records = await _fetch(db, "arcgis_landing")
|
||||
|
||||
# Load relation + cable data for cable_names mapping
|
||||
rel_result = await db.execute(
|
||||
select(CollectedData)
|
||||
.where(CollectedData.source == "arcgis_relation")
|
||||
.where(CollectedData.is_current.is_(True))
|
||||
)
|
||||
rel_records = list(rel_result.scalars().all())
|
||||
|
||||
cable_result = await db.execute(
|
||||
select(CollectedData)
|
||||
.where(CollectedData.source == "telegeography_cables")
|
||||
.where(CollectedData.is_current.is_(True))
|
||||
)
|
||||
cable_records = list(cable_result.scalars().all())
|
||||
|
||||
# Build mapping: city_id → list of cable names
|
||||
city_to_cable_ids: Dict[int, List[int]] = {}
|
||||
for r in rel_records:
|
||||
meta = r.extra_data or {}
|
||||
city_id = meta.get("city_id")
|
||||
cable_id = meta.get("cable_id")
|
||||
if city_id is not None and cable_id is not None:
|
||||
city_to_cable_ids.setdefault(city_id, [])
|
||||
if cable_id not in city_to_cable_ids[city_id]:
|
||||
city_to_cable_ids[city_id].append(cable_id)
|
||||
|
||||
cable_id_to_name: Dict[int, str] = {}
|
||||
for r in cable_records:
|
||||
meta = r.extra_data or {}
|
||||
cable_id = meta.get("cable_id")
|
||||
if cable_id and r.name:
|
||||
cable_id_to_name[cable_id] = r.name
|
||||
|
||||
items = []
|
||||
for record in lp_records:
|
||||
lat = _safe_float(get_record_field(record, "latitude"))
|
||||
lon = _safe_float(get_record_field(record, "longitude"))
|
||||
if lat is None or lon is None:
|
||||
continue
|
||||
meta = record.extra_data or {}
|
||||
city_id = meta.get("city_id")
|
||||
cable_names = []
|
||||
if city_id in city_to_cable_ids:
|
||||
cable_names = [
|
||||
cable_id_to_name[cid]
|
||||
for cid in city_to_cable_ids[city_id]
|
||||
if cid in cable_id_to_name
|
||||
]
|
||||
items.append({
|
||||
"id": f"lp_{record.id}",
|
||||
"name": record.name or "Unknown",
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"country": get_record_field(record, "country") or "",
|
||||
"city": get_record_field(record, "city") or "",
|
||||
"cable_names": cable_names,
|
||||
})
|
||||
return {"count": len(items), "items": items}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cables (route geometry)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/cables")
|
||||
async def ue_cables(db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Returns cable route geometry.
|
||||
|
||||
Each segment is a flat array of [lon, lat] pairs.
|
||||
|
||||
Response shape:
|
||||
{
|
||||
"count": 100,
|
||||
"items": [
|
||||
{
|
||||
"id": "cable_42",
|
||||
"cable_id": "flag",
|
||||
"name": "FLAG",
|
||||
"status": "active",
|
||||
"length_km": 28000,
|
||||
"segments": [
|
||||
[[lon, lat], [lon, lat], ...]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
records = await _fetch(db, "telegeography_cables")
|
||||
items = []
|
||||
for record in records:
|
||||
meta = record.extra_data or {}
|
||||
route_coords = meta.get("route_coordinates", [])
|
||||
segments: List[List[List[float]]] = []
|
||||
|
||||
if route_coords:
|
||||
# Support both flat [lon,lat] array and array-of-arrays
|
||||
if route_coords and isinstance(route_coords[0][0], list):
|
||||
raw_lines = route_coords
|
||||
else:
|
||||
raw_lines = [route_coords]
|
||||
|
||||
for raw_line in raw_lines:
|
||||
line = []
|
||||
for pt in raw_line:
|
||||
try:
|
||||
line.append([float(pt[0]), float(pt[1])])
|
||||
except (TypeError, ValueError, IndexError):
|
||||
continue
|
||||
if len(line) >= 2:
|
||||
segments.append(line)
|
||||
|
||||
if not segments:
|
||||
continue
|
||||
|
||||
items.append({
|
||||
"id": f"cable_{record.id}",
|
||||
"cable_id": record.source_id or record.name or "",
|
||||
"name": record.name or "Unknown",
|
||||
"status": meta.get("status", "active"),
|
||||
"length_km": _safe_float(get_record_field(record, "value")),
|
||||
"owners": meta.get("owners") or [],
|
||||
"rfs": meta.get("rfs"),
|
||||
"color": meta.get("color"),
|
||||
"segments": segments,
|
||||
})
|
||||
return {"count": len(items), "items": items}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Satellites (TLE data)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/satellites")
|
||||
async def ue_satellites(
|
||||
limit: int = Query(default=200, ge=1, le=5000),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Returns satellite TLE data for orbit propagation in UE.
|
||||
|
||||
Response shape:
|
||||
{
|
||||
"count": 200,
|
||||
"items": [
|
||||
{
|
||||
"id": "sat_42",
|
||||
"norad_id": "25544",
|
||||
"name": "ISS (ZARYA)",
|
||||
"tle_line1": "1 25544U ...",
|
||||
"tle_line2": "2 25544 ...",
|
||||
"epoch": "2026-04-14T00:00:00Z",
|
||||
"inclination": 51.6,
|
||||
"mean_motion": 15.5
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
records = await _fetch(db, "celestrak", limit)
|
||||
items = []
|
||||
for record in records:
|
||||
meta = record.extra_data or {}
|
||||
norad_id = meta.get("norad_cat_id")
|
||||
if not norad_id:
|
||||
continue
|
||||
tle1 = meta.get("tle_line1")
|
||||
tle2 = meta.get("tle_line2")
|
||||
if not tle1 or not tle2:
|
||||
tle1, tle2 = build_tle_lines_from_elements(
|
||||
norad_cat_id=norad_id,
|
||||
epoch=meta.get("epoch"),
|
||||
inclination=meta.get("inclination"),
|
||||
raan=meta.get("raan"),
|
||||
eccentricity=meta.get("eccentricity"),
|
||||
arg_of_perigee=meta.get("arg_of_perigee"),
|
||||
mean_anomaly=meta.get("mean_anomaly"),
|
||||
mean_motion=meta.get("mean_motion"),
|
||||
)
|
||||
items.append({
|
||||
"id": f"sat_{record.id}",
|
||||
"norad_id": str(norad_id),
|
||||
"name": record.name or "Unknown",
|
||||
"tle_line1": tle1 or "",
|
||||
"tle_line2": tle2 or "",
|
||||
"epoch": meta.get("epoch") or "",
|
||||
"inclination": _safe_float(meta.get("inclination")),
|
||||
"raan": _safe_float(meta.get("raan")),
|
||||
"eccentricity": _safe_float(meta.get("eccentricity")),
|
||||
"mean_motion": _safe_float(meta.get("mean_motion")),
|
||||
})
|
||||
return {"count": len(items), "items": items}
|
||||
144
docs/ue5_led_context.md
Normal file
144
docs/ue5_led_context.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 项目背景:UE5 + 3D LED 大屏展示系统
|
||||
|
||||
> 供其他 AI 快速了解项目背景和当前状态。
|
||||
> 最后更新:2026-04-14
|
||||
|
||||
---
|
||||
|
||||
## 一、项目定位
|
||||
|
||||
这不是一个普通的桌面地球应用,而是一套**领导演示用的 3D 沉浸式展示系统**。
|
||||
|
||||
核心逻辑:
|
||||
- 态势感知数据(超算、海缆、卫星)是内容
|
||||
- 3D LED 大屏 + 实时渲染 + 动捕交互是"醋"——没有它,内容再好也只是普通屏幕
|
||||
- 主要受众:领导/决策层,注重视觉冲击力
|
||||
|
||||
---
|
||||
|
||||
## 二、硬件配置
|
||||
|
||||
| 硬件 | 规格 | 说明 |
|
||||
|------|------|------|
|
||||
| 3D LED 大屏 | 5.12m × 2.88m,P1.538mm,被动偏振式 | 观众戴无源 3D 眼镜(≤18g),60Hz 即可出稳定 3D |
|
||||
| 渲染工作站 | 双路国产 X86 CPU + RTX 5090 32GB | 驱动 UE5 实时渲染 |
|
||||
| 视频处理器 | 随屏配套(品牌待确认) | 接收 GPU 信号,驱动 LED 墙 |
|
||||
| 动捕摄像头 | RGB 摄像头 ×2,4K,直连电脑 | 无穿戴姿态识别 |
|
||||
| 音响 | 解码功放 + 吸顶喇叭 ×5 + 低音炮 | 配套 |
|
||||
|
||||
---
|
||||
|
||||
## 三、软件架构
|
||||
|
||||
```
|
||||
摄像头(×2,直连)
|
||||
↓ 动捕插件(供应商提供 UE5 插件)
|
||||
UE5 主程序(我们开发)
|
||||
├── Cesium 地球(实时渲染)
|
||||
├── 超算数据点(可交互)
|
||||
├── 其他可交互物件
|
||||
└── 立体渲染输出
|
||||
↓ 视频信号(格式待确认)
|
||||
视频处理器(随屏配套)
|
||||
↓ LED 驱动信号(行偏振)
|
||||
5m 被动偏振 3D LED 大屏
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、分工
|
||||
|
||||
| 部分 | 谁做 | 状态 |
|
||||
|------|------|------|
|
||||
| LED 屏体 + 视频处理器 | 屏幕供应商 | 采购中 |
|
||||
| 动捕插件(UE5 插件形式) | 动捕供应商 | 待交付 |
|
||||
| UE5 基础工程(关卡+角色+动捕绑定) | 动捕供应商 | 待交付 |
|
||||
| 地球场景 + 数据可视化 + 交互逻辑 | 我们(本项目) | 开发中 |
|
||||
| 后端数据接口 | 我们(本项目) | 已完成 |
|
||||
| 其他 9 个定制 3D 资产和动画 | 3D 内容供应商 | 采购中 |
|
||||
|
||||
---
|
||||
|
||||
## 五、动捕交互方式
|
||||
|
||||
**交付形式(已确认)**:
|
||||
- 供应商给我们**完整 UE5 工程**,包含:
|
||||
- 视频动捕插件
|
||||
- 已配好绑定和重定向的 3D 角色
|
||||
- 3D 模型和动画资产
|
||||
- 接两台摄像头即可直接运行
|
||||
- **我们的任务**:把他们工程的内容(插件 + 角色 + 资产)**迁移进我们的 `ue_client/` 工程**,然后把角色动作映射到地球操作
|
||||
|
||||
**待确认**:角色动作的触发点是什么形式?
|
||||
- 蓝图 Custom Event(如 `OnGestureRotate`)?
|
||||
- AnimNotify?
|
||||
- 需要我们自己判断骨骼姿态?
|
||||
|
||||
**交互目标(一期)**:
|
||||
1. 手势旋转地球
|
||||
2. 手势缩放地球
|
||||
3. 手势指向/确认 → 选中数据点,弹出信息卡
|
||||
4. 鼠标作为备用输入(始终可用)
|
||||
|
||||
---
|
||||
|
||||
## 六、立体渲染
|
||||
|
||||
**待确认**:
|
||||
1. 供应商基础工程里是否已配好立体渲染输出?
|
||||
2. 如果没有:视频处理器接受什么格式?(Side-by-Side / Top-Bottom / 其他)
|
||||
3. 给供应商的文件形式:UE5 工程 / .exe / 视频文件 / 直连实时输出?
|
||||
|
||||
**已准备**:`StereoRenderingManager.h/.cpp` 支持运行时切换 SbS/TbB,格式确认后直接启用。
|
||||
|
||||
---
|
||||
|
||||
## 七、已完成的代码
|
||||
|
||||
### 后端 (`backend/app/api/v1/ue_data.py`)
|
||||
- `GET /api/v1/ue/status` — 健康检查
|
||||
- `GET /api/v1/ue/compute-points` — TOP500 超算(平铺 JSON)
|
||||
- `GET /api/v1/ue/landing-points` — 海缆登陆点
|
||||
- `GET /api/v1/ue/cables` — 海缆路由几何
|
||||
- `GET /api/v1/ue/satellites` — 卫星 TLE 数据
|
||||
|
||||
### UE5 C++ (`ue_client/Source/PlanetClient/`)
|
||||
|
||||
| 文件 | 功能 |
|
||||
|------|------|
|
||||
| `PlanetDataTypes.h` | FComputePoint 等数据结构 |
|
||||
| `PlanetDataManager` | HTTP 拉取 + mock 数据 + 生成 Actor |
|
||||
| `ComputePointActor` | 超算点 Actor,三态材质(正常/悬停/选中)|
|
||||
| `InteractiveObjectBase` | 所有可交互物件的基类 |
|
||||
| `GlobeInteractionComponent` | 拖拽旋转地球(改 Cesium 经纬度原点)+ 缩放,带惯性 |
|
||||
| `StereoRenderingManager` | 立体渲染开关,SbS/TbB,IPD 可调 |
|
||||
| `MotionCaptureInterface.h` | 动捕接口抽象(待用插件 API 替换实现)|
|
||||
| `PlanetPlayerController` | 统一处理鼠标 + 动捕输入 |
|
||||
| `PlanetGameMode` | 场景入口 |
|
||||
|
||||
### 文档
|
||||
- `docs/ue_client_setup_guide.md` — 编辑器操作 step-by-step 指南
|
||||
- `docs/ue_todo.md` — 待供应商回复的 TODO
|
||||
- `docs/ue5_mvp_fused_plan.md` — 完整实施方案(v3.0)
|
||||
|
||||
---
|
||||
|
||||
## 八、交付时间线
|
||||
|
||||
| 时间 | 内容 | 状态 |
|
||||
|------|------|------|
|
||||
| 本周末前 | 动捕供应商:含动捕插件 + 3D 角色(绑定/重定向已配好)的基础 UE5 工程 | 等待中 |
|
||||
| 之后尽快 | 动捕供应商:动画资产(复制进 Content/ 即可直接调用) | 等待中 |
|
||||
| TBD | LED 屏供应商:视频处理器接受的 3D 信号格式(或直接技术支持对接) | 等待中 |
|
||||
|
||||
**拿到基础工程后可立即做**:迁移插件和角色,接摄像头做动捕调试,然后对接动作→地球操作映射。
|
||||
|
||||
**双目 3D 显示**:供应商可提供技术支持,等视频处理器到位后直接对接。
|
||||
|
||||
---
|
||||
|
||||
## 九、当前阻塞项
|
||||
|
||||
1. **动捕基础工程**(本周末前到)→ 迁移内容,确认动作触发方式,完成交互对接
|
||||
2. **动画资产**(尽快)→ 复制入 Content/,在场景中引用
|
||||
3. **3D 显示格式**(有供应商技术支持)→ 配置 `StereoRenderingManager`
|
||||
File diff suppressed because it is too large
Load Diff
319
docs/ue_client_setup_guide.md
Normal file
319
docs/ue_client_setup_guide.md
Normal file
@@ -0,0 +1,319 @@
|
||||
# UE5 客户端手动操作指南
|
||||
|
||||
> 本指南对应 `ue_client/` 目录下已生成的所有代码和配置。
|
||||
> 代码已写好,你只需要做编辑器里的点击操作。
|
||||
> 遇到红色错误先看文末"常见问题"章节。
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
| 软件 | 版本 | 下载地址 |
|
||||
|------|------|---------|
|
||||
| Unreal Engine | **5.3** | Epic Games Launcher → Library → 5.3 |
|
||||
| Cesium for Unreal | 最新 | 直接在下一步从 Marketplace 安装 |
|
||||
| Visual Studio | 2022 Community | visualstudio.microsoft.com(安装 C++ 游戏开发工作负载) |
|
||||
|
||||
> **注意**:Cesium for Unreal 必须先安装,否则代码会编译失败。
|
||||
|
||||
---
|
||||
|
||||
## 第一阶段(Phase A):本地演示版(不需要后端)
|
||||
|
||||
### 步骤 1:安装 Cesium for Unreal
|
||||
|
||||
1. 打开 **Epic Games Launcher**
|
||||
2. 顶部切换到 **Unreal Engine** 选项卡
|
||||
3. 左侧点击 **Fab**(原 Marketplace,已改名)→ 搜索 `Cesium for Unreal`
|
||||
4. 点击 **免费获取**(Free),然后点击 **安装到引擎** → 选择 5.3
|
||||
5. 等待安装完成
|
||||
|
||||
---
|
||||
|
||||
### 步骤 2:打开项目
|
||||
|
||||
1. 打开 **Epic Games Launcher** → **Unreal Engine** → **Library**
|
||||
2. 找到 5.3,点击右侧 **Launch** 旁边的下拉箭头 → **Browse**
|
||||
3. 导航到 `planet/ue_client/`,选择 `PlanetClient.uproject`,点击打开
|
||||
4. UE 会提示"缺少模块,需要重新编译" → 点击 **Yes**
|
||||
5. 等待编译完成(首次约 5-10 分钟)
|
||||
|
||||
> 如果编译报错:见文末"常见问题 → 编译错误"
|
||||
|
||||
---
|
||||
|
||||
### 步骤 3:创建新关卡
|
||||
|
||||
1. 菜单栏 → **File** → **New Level**
|
||||
2. 选择 **Empty Level**(空关卡)
|
||||
3. 保存:**File** → **Save Current Level As**
|
||||
路径:`Content/Maps/`,名称:`EarthMap`
|
||||
4. 点击 **Save**
|
||||
|
||||
---
|
||||
|
||||
### 步骤 4:添加 Cesium 地球
|
||||
|
||||
1. 顶部菜单栏 → **Cesium**(如果没有此菜单,说明插件未激活:Edit → Plugins → 搜索 Cesium → 勾选 Enable → 重启)
|
||||
2. 在 Cesium 面板里点击 **Add Blank 3D Tiles Tileset** — 这会自动在场景里添加:
|
||||
- `CesiumGeoreference` Actor
|
||||
- `Cesium3DTileset` Actor(地球瓦片)
|
||||
3. 再点击 **Add Cesium ion Bing Maps Aerial** 添加卫星影像底图(需要免费 Cesium ion 账号)
|
||||
|
||||
> 如果没有 Cesium ion 账号:Cesium 菜单 → Connect to Cesium ion → 注册免费账号
|
||||
|
||||
---
|
||||
|
||||
### 步骤 5:添加相机 Pawn
|
||||
|
||||
1. 菜单栏 → **Cesium** → 找到 **Dynamic Pawn**(名称可能是 `CesiumFlyToComponent` 相关的 Blueprint)
|
||||
**或者**:Content Browser → 顶部搜索框输入 `DynamicPawn` → 找到插件内容里的 `DynamicPawn` → 拖入场景
|
||||
2. 在 **Outliner** 面板里点击刚拖入的 `DynamicPawn`
|
||||
3. 在 **Details** 面板里,找到 **Auto Possess Player** → 改为 **Player 0**
|
||||
|
||||
> 这样游戏启动时摄像机会自动使用这个可飞行的地球相机。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 6:配置 GameMode
|
||||
|
||||
1. 菜单栏 → **Window** → **World Settings**(如果没有,也可以在 Details Panel 里找)
|
||||
2. 在 **World Settings** 面板里找到 **Game Mode Override**
|
||||
3. 点击下拉框 → 搜索 `PlanetGameMode` → 选择它
|
||||
4. 找到 **Default Pawn Class** → 改为上一步拖入的 `DynamicPawn`
|
||||
|
||||
---
|
||||
|
||||
### 步骤 7:创建 ComputePoint Blueprint
|
||||
|
||||
这步把我写的 C++ 类包装成可以在编辑器里配置材质的 Blueprint。
|
||||
|
||||
1. **Content Browser** → 空白处右键 → **Blueprint Class**
|
||||
2. 搜索父类:输入 `ComputePointActor` → 找到 `AComputePointActor` → 点击 **Select**
|
||||
3. 命名为 `BP_ComputePointActor`,保存到 `Content/Blueprints/`
|
||||
|
||||
**配置 BP_ComputePointActor 的网格和材质:**
|
||||
|
||||
4. 双击打开 `BP_ComputePointActor`
|
||||
5. 在左侧 **Components** 面板里点击 `SphereMesh`
|
||||
6. 在右侧 **Details** 面板里找到 **Static Mesh** → 点击下拉 → 搜索 `Sphere` → 选择 **Engine/BasicShapes/Sphere**
|
||||
7. 找到 **Material** → 点击下拉 → 搜索 `M_Basic_Wall` 或者创建新材质(见下方)
|
||||
|
||||
**创建三种状态的材质(颜色点即可):**
|
||||
|
||||
8. Content Browser → 右键 → **Material** → 命名 `M_PointNormal`
|
||||
- 双击打开 → 右键空白区域 → 搜索 `Constant3Vector` → 连接到 `Base Color`
|
||||
- 颜色设为橙色:`(1.0, 0.4, 0.0)`
|
||||
- 保存
|
||||
|
||||
9. 同样方式创建 `M_PointHovered`(颜色白色 `1,1,1`)和 `M_PointSelected`(颜色青色 `0,1,1`)
|
||||
|
||||
**回到 BP_ComputePointActor:**
|
||||
|
||||
10. 在 **Details** 面板里:
|
||||
- **Normal Material** → 选 `M_PointNormal`
|
||||
- **Hovered Material** → 选 `M_PointHovered`
|
||||
- **Selected Material** → 选 `M_PointSelected`
|
||||
- **Point Scale** → `80000`(根据实际效果调整)
|
||||
11. 点击左上角 **Compile** → **Save**
|
||||
|
||||
---
|
||||
|
||||
### 步骤 8:放置 DataManager 并配置
|
||||
|
||||
1. **Content Browser** → 右键 → **Blueprint Class** → 父类搜索 `PlanetDataManager` → 选择 `APlanetDataManager`
|
||||
2. 命名为 `BP_PlanetDataManager`,保存到 `Content/Blueprints/`
|
||||
3. 将 `BP_PlanetDataManager` **拖入场景**(Outliner 里会出现它)
|
||||
4. 在 Outliner 里点击它 → 在 **Details** 面板里配置:
|
||||
- **Use Local Mock Data** → ✅ **勾选**(Phase A 不需要后端)
|
||||
- **Mock Data Path** → 留空(代码会自动找 `Content/Data/mock_compute_points.json`)
|
||||
- **Compute Point Class** → 选择 `BP_ComputePointActor`
|
||||
- **Point Altitude Meters** → `50000`(海拔 50km,可调)
|
||||
|
||||
---
|
||||
|
||||
### 步骤 9:测试 Phase A
|
||||
|
||||
1. 点击顶部工具栏绿色 **Play** 按钮(或 Alt+P)
|
||||
2. 地球应该加载卫星影像
|
||||
3. 应该看到 10 个橙色球体分布在地球上(对应 mock JSON 里的 TOP500 超算)
|
||||
4. 鼠标移到球体上 → 变白色(Hover)
|
||||
5. 点击球体 → 变青色(Selected)
|
||||
|
||||
**验证通过标准:**
|
||||
- [x] 地球可见
|
||||
- [x] 橙色球体出现在正确位置(美国、日本、荷兰、芬兰等)
|
||||
- [x] 悬停变色
|
||||
- [x] 点击变色
|
||||
|
||||
---
|
||||
|
||||
## 第二阶段(Phase B):连接真实后端
|
||||
|
||||
### 步骤 10:确认后端新接口可用
|
||||
|
||||
后端代码已添加新路由,先验证它已经运行:
|
||||
|
||||
```bash
|
||||
# 在 WSL 或终端里
|
||||
curl http://localhost:8000/api/v1/ue/status
|
||||
```
|
||||
|
||||
应该返回类似:
|
||||
```json
|
||||
{"ok": true, "server_time": "...", "compute_points_count": 500, ...}
|
||||
```
|
||||
|
||||
如果 curl 失败:
|
||||
- 检查 `planet.sh` 是否在运行(`./planet.sh start`)
|
||||
- 检查 WSL2 → Windows 的网络:在 UE 里使用 `172.x.x.x`(WSL 网关地址)而不是 `localhost`
|
||||
|
||||
---
|
||||
|
||||
### 步骤 11:获取 WSL2 → Windows 的正确 IP
|
||||
|
||||
在 WSL 终端里运行:
|
||||
|
||||
```bash
|
||||
cat /etc/resolv.conf | grep nameserver | awk '{print $2}'
|
||||
```
|
||||
|
||||
记下这个 IP(例如 `172.22.32.1`)。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 12:切换 DataManager 到实时模式
|
||||
|
||||
1. 在 Outliner 里点击 `BP_PlanetDataManager`
|
||||
2. Details 面板里:
|
||||
- **Use Local Mock Data** → **取消勾选**
|
||||
- **Backend Base URL** → 填入 `http://172.22.32.1:8000`(你的实际 WSL IP)
|
||||
3. 重新 Play → DataManager 会通过 HTTP 拉取真实数据
|
||||
|
||||
---
|
||||
|
||||
### 步骤 13:绑定点击事件显示信息卡(可选,需要 UMG)
|
||||
|
||||
这步是可选的,需要创建一个 Widget Blueprint 来显示选中点的信息。
|
||||
|
||||
1. Content Browser → 右键 → **User Interface** → **Widget Blueprint**
|
||||
2. 命名为 `WBP_PointInfo`
|
||||
|
||||
**设计 Widget 布局:**
|
||||
|
||||
3. 双击打开 `WBP_PointInfo`
|
||||
4. 从左侧 **Palette** 拖入以下控件到画布:
|
||||
- `Canvas Panel`(容器,设置为全屏)
|
||||
- `Border`(右下角定位,用作信息卡背景,宽 300,高 200)
|
||||
- `Text Block` × 4(名称、排名、算力、国家)
|
||||
|
||||
**绑定事件(Blueprint 里操作):**
|
||||
|
||||
5. 打开 `BP_PlanetDataManager` 的 Event Graph
|
||||
6. 找到 **BeginPlay** 节点
|
||||
7. 拖出线 → 搜索 **Bind Event to On Point Selected**(这是我在 PlayerController 里定义的委托)
|
||||
|
||||
> 具体蓝图连线:从 PlayerController 获取 OnPointSelected → Bind → 在回调里 Create Widget WBP_PointInfo → Add to Viewport → Set 各个文本
|
||||
|
||||
---
|
||||
|
||||
## 文件结构总览
|
||||
|
||||
```
|
||||
ue_client/
|
||||
PlanetClient.uproject ← UE 项目入口
|
||||
Config/
|
||||
DefaultGame.ini ← GameMode 配置
|
||||
DefaultEngine.ini ← 渲染/引擎设置
|
||||
DefaultInput.ini ← 键鼠输入绑定
|
||||
Content/
|
||||
Data/
|
||||
mock_compute_points.json ← Phase A 本地测试数据(10个超算)
|
||||
Maps/
|
||||
EarthMap.umap ← 你在步骤3创建的关卡
|
||||
Blueprints/
|
||||
BP_ComputePointActor ← 步骤7创建
|
||||
BP_PlanetDataManager ← 步骤8创建
|
||||
Source/
|
||||
PlanetClient/
|
||||
PlanetClient.Build.cs ← 模块依赖(HTTP、JSON、Cesium)
|
||||
PlanetClient.h/.cpp ← 模块入口
|
||||
PlanetDataTypes.h ← 数据结构定义(FComputePoint 等)
|
||||
PlanetDataManager.h/.cpp ← HTTP 拉取 + 生成 Actor
|
||||
ComputePointActor.h/.cpp ← 单个超算点的可视化 Actor
|
||||
PlanetPlayerController.h/.cpp ← 鼠标点击、悬停、相机控制
|
||||
PlanetGameMode.h/.cpp ← GameMode 入口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端新接口一览
|
||||
|
||||
后端已新增以下接口(无需认证,直接访问):
|
||||
|
||||
| 接口 | 说明 |
|
||||
|------|------|
|
||||
| `GET /api/v1/ue/status` | 健康检查 + 各数据源数量 |
|
||||
| `GET /api/v1/ue/compute-points` | TOP500 超算数据(平铺 JSON) |
|
||||
| `GET /api/v1/ue/landing-points` | 海缆登陆点(平铺 JSON) |
|
||||
| `GET /api/v1/ue/cables` | 海缆路由几何(segments 数组) |
|
||||
| `GET /api/v1/ue/satellites` | 卫星 TLE 数据 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 编译报错 "Cannot open include file: CesiumGeoreference.h"
|
||||
**A:** Cesium for Unreal 没有正确安装,或者没有在 `.uproject` 里启用。检查:
|
||||
1. Epic Launcher → 插件是否安装到 5.3
|
||||
2. `PlanetClient.uproject` 里 `Plugins` 数组是否有 `CesiumForUnreal: true`
|
||||
3. UE 编辑器 → Edit → Plugins → 搜索 Cesium → 确认已勾选 Enabled
|
||||
|
||||
### Q: Play 之后没有看到橙色球体
|
||||
**A:** 按以下顺序排查:
|
||||
1. Output Log(Window → Output Log)里搜索 `PlanetDataManager` — 查看是否有报错
|
||||
2. 检查 `BP_PlanetDataManager` 的 Details → **Compute Point Class** 是否已设置为 `BP_ComputePointActor`
|
||||
3. 检查 **Mock Data Path** 是否正确(留空则自动用 `Content/Data/mock_compute_points.json`)
|
||||
4. 检查 **Use Local Mock Data** 是否已勾选
|
||||
|
||||
### Q: 地球是灰色的没有卫星影像
|
||||
**A:** 需要 Cesium ion 账号:
|
||||
1. Cesium 菜单 → Connect to Cesium ion
|
||||
2. 注册免费账号并授权
|
||||
3. 重新添加 **Cesium ion Bing Maps Aerial** tileset
|
||||
|
||||
### Q: WSL2 里的后端 UE 无法访问(Phase B)
|
||||
**A:** WSL2 和 Windows 是不同网络命名空间。方法:
|
||||
1. 在 WSL 里运行 `ip route show default | awk '{print $3}'` — 这是 Windows 主机的 IP
|
||||
2. 后端绑定到 `0.0.0.0:8000`(检查 `uvicorn` 启动参数,应已如此配置)
|
||||
3. 在 UE 的 DataManager 里填写这个 IP 而不是 `localhost`
|
||||
|
||||
### Q: TransformLongitudeLatitudeHeightPositionToUnreal 不存在
|
||||
**A:** Cesium for Unreal API 在不同版本有变化。如果编译报此错,将 `PlanetDataManager.cpp` 里的调用改为:
|
||||
```cpp
|
||||
// Cesium for Unreal v1.x 的旧 API:
|
||||
FVector WorldPos = Georeference->TransformLongitudeLatitudeHeightToUnreal(
|
||||
Pt.Longitude, Pt.Latitude, PointAltitudeMeters);
|
||||
|
||||
// 或者通过 GeoTransforms:
|
||||
#include "CesiumGlobeAnchorComponent.h"
|
||||
// ... 见 Cesium 文档
|
||||
```
|
||||
|
||||
### Q: 点击球体没有反应
|
||||
**A:**
|
||||
1. 确认 `SphereMesh` 上 **Collision Presets** 不是 `NoCollision`
|
||||
- 打开 `BP_ComputePointActor` → 点击 `SphereMesh` → Details → Collision → 改为 `BlockAllDynamic`
|
||||
2. 确认 PlayerController 的 `bEnableClickEvents = true`(代码里已设置)
|
||||
3. Output Log 里搜索是否有输入相关报错
|
||||
|
||||
---
|
||||
|
||||
## 下一步(一期完成后)
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 海缆路径渲染 | `/api/v1/ue/cables` 已就绪,需要在 UE 里用 Spline 绘制 |
|
||||
| 信息卡 UMG | 创建 Widget Blueprint 并在 PlayerController OnPointSelected 里显示 |
|
||||
| WebSocket 实时更新 | 后端已有 WebSocket,UE 端需要使用 WebSockets 插件 |
|
||||
| 卫星轨迹 | TLE 数据已就绪,需要在 UE 里做轨道传播计算 |
|
||||
90
docs/ue_todo.md
Normal file
90
docs/ue_todo.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# UE5 客户端 — 待供应商回复的 TODO
|
||||
|
||||
> 以下两个问题答复后,对应代码可在一天内完成。
|
||||
> 其余所有代码均已写好,不依赖这两个答案。
|
||||
|
||||
---
|
||||
|
||||
## TODO-1:视频处理器输入格式
|
||||
|
||||
**等待信息**:LED 屏配套视频处理器(如诺瓦星云)接受什么 3D 信号格式?
|
||||
|
||||
| 可能答案 | 对应操作 |
|
||||
|---------|---------|
|
||||
| Side-by-Side(左右并排) | `StereoRenderingManager.cpp` 里 `EnableStereo` 的 `SideBySide` 分支已写好,直接启用 |
|
||||
| Top-Bottom(上下叠加) | 同上,切换到 `TopBottom` 分支 |
|
||||
| 行交错(行偏振直驱) | 需要新写一个 PostProcess Material,把左右眼奇偶行合并输出 |
|
||||
| 私有协议 | 需要供应商提供 UE5 插件或信号格式文档 |
|
||||
|
||||
**代码位置**:
|
||||
```
|
||||
ue_client/Source/PlanetClient/StereoRenderingManager.h — 第 8-18 行 TODO 注释
|
||||
ue_client/Source/PlanetClient/StereoRenderingManager.cpp — EnableStereo() 函数
|
||||
```
|
||||
|
||||
**需要同时确认**:
|
||||
- 视频处理器品牌和型号
|
||||
- 屏幕物理分辨率(用于配置 UE5 输出分辨率)
|
||||
- 3D 启用时是否需要特殊信号时序(如 3D Frame Packing)
|
||||
|
||||
---
|
||||
|
||||
## TODO-2:动捕角色接入
|
||||
|
||||
**已确认**:
|
||||
- **本周末前**:供应商交付含动捕插件 + 3D 角色(已配好绑定和重定向)的基础 UE5 工程
|
||||
- 接入两台摄像头,4080 以上显卡即可直接运行动捕调试
|
||||
- **之后尽快**:动画资产单独交付,复制到工程 Content/ 目录即可直接调用
|
||||
- **我们的任务**:把他们工程内容迁移进 `ue_client/`,把角色动作映射到地球操作
|
||||
|
||||
---
|
||||
|
||||
### 阶段 A:拿到基础工程后(本周末)
|
||||
|
||||
**迁移步骤**:
|
||||
|
||||
1. **迁移动捕插件**
|
||||
- 从供应商工程 `Plugins/` 拷到 `ue_client/Plugins/`
|
||||
- `PlanetClient.uproject` 的 `Plugins` 数组添加插件条目(`Enabled: true`)
|
||||
- `PlanetClient.Build.cs` 的 `PublicDependencyModuleNames` 加插件模块名
|
||||
|
||||
2. **迁移角色**
|
||||
- 把角色 Blueprint、动画、骨骼网格从供应商 `Content/` 拷到 `ue_client/Content/`
|
||||
- 在关卡里放置角色 Actor,确认摄像头接入后能正常驱动
|
||||
|
||||
3. **接入动作触发(关键,看到工程后确认方式)**:
|
||||
|
||||
| 需要确认的内容 | 用途 |
|
||||
|-------------|------|
|
||||
| 角色动作怎么暴露给外部? | Blueprint Custom Event / AnimNotify / 骨骼姿态变量? |
|
||||
| 手势集合有哪些? | 填写 `EMotionGesture` 枚举,配置 `FMotionActionMapping` |
|
||||
| 是否持续输出旋转增量? | 旋转地球用"持续增量"还是"离散手势触发" |
|
||||
| 插件模块名称(`ModuleName`)| 加入 `Build.cs` 依赖 |
|
||||
|
||||
**拿到工程后(我来做)**:
|
||||
- 用插件实际 API 实现 `UMotionCaptureReceiver` 子类
|
||||
- 在 `PlanetPlayerController::BeginPlay` 里取消注释 `BindMotionCaptureEvents()`
|
||||
|
||||
**代码位置(接口已预留)**:
|
||||
```
|
||||
ue_client/Source/PlanetClient/MotionCaptureInterface.h — 基类和手势枚举
|
||||
ue_client/Source/PlanetClient/PlanetPlayerController.cpp — BindMotionCaptureEvents()
|
||||
ue_client/Source/PlanetClient/PlanetClient.Build.cs — TODO 注释处加插件模块名
|
||||
ue_client/PlanetClient.uproject — Plugins 数组加插件条目
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 B:动画资产到位后
|
||||
|
||||
- 把供应商提供的动画资产直接复制到 `ue_client/Content/` 对应目录
|
||||
- 在 `PlanetDataManager` / 场景 Actor 里引用这些资产即可调用
|
||||
|
||||
---
|
||||
|
||||
## 答复到位后的操作清单
|
||||
|
||||
拿到答案后告诉我,我来:
|
||||
|
||||
1. **视频处理器格式** → 配置 `StereoRenderingManager`,把 `bAutoEnableOnPlay` 改为 `true`,写进 setup guide
|
||||
2. **动捕插件** → 插件放入 `ue_client/Plugins/`,实现 `UMotionCaptureReceiver` 子类对接插件 API,更新 `Build.cs` 和 `.uproject`
|
||||
10
ue_client/Config/DefaultEngine.ini
Normal file
10
ue_client/Config/DefaultEngine.ini
Normal file
@@ -0,0 +1,10 @@
|
||||
[/Script/Engine.RendererSettings]
|
||||
r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True
|
||||
r.AntiAliasingMethod=4
|
||||
|
||||
[/Script/Engine.Engine]
|
||||
NearClipPlane=1.0
|
||||
|
||||
[Core.System]
|
||||
Paths=../../../Engine/Content
|
||||
Paths=%GAMEDIR%Content
|
||||
3
ue_client/Config/DefaultGame.ini
Normal file
3
ue_client/Config/DefaultGame.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[/Script/EngineSettings.GameMapsSettings]
|
||||
GlobalDefaultGameMode=/Script/PlanetClient.PlanetGameMode
|
||||
GameDefaultMap=/Game/Maps/EarthMap
|
||||
6
ue_client/Config/DefaultInput.ini
Normal file
6
ue_client/Config/DefaultInput.ini
Normal file
@@ -0,0 +1,6 @@
|
||||
[/Script/Engine.InputSettings]
|
||||
+ActionMappings=(ActionName="SelectPoint",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=LeftMouseButton)
|
||||
+ActionMappings=(ActionName="RightMouseDown",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=RightMouseButton)
|
||||
+AxisMappings=(AxisName="ZoomCamera",Scale=-1.0,Key=MouseWheelAxis)
|
||||
+AxisMappings=(AxisName="RotateYaw",Scale=1.0,Key=MouseX)
|
||||
+AxisMappings=(AxisName="RotatePitch",Scale=-1.0,Key=MouseY)
|
||||
135
ue_client/Content/Data/mock_compute_points.json
Normal file
135
ue_client/Content/Data/mock_compute_points.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"count": 10,
|
||||
"items": [
|
||||
{
|
||||
"id": "top500_1",
|
||||
"name": "Frontier",
|
||||
"latitude": 36.01,
|
||||
"longitude": -84.26,
|
||||
"country": "United States",
|
||||
"city": "Oak Ridge",
|
||||
"rank": 1,
|
||||
"rmax_tflops": 1194000.0,
|
||||
"rpeak_tflops": 1679616.0,
|
||||
"cores": 8730112,
|
||||
"power_kw": 22703.0
|
||||
},
|
||||
{
|
||||
"id": "top500_2",
|
||||
"name": "Aurora",
|
||||
"latitude": 41.71,
|
||||
"longitude": -87.98,
|
||||
"country": "United States",
|
||||
"city": "Lemont",
|
||||
"rank": 2,
|
||||
"rmax_tflops": 1012000.0,
|
||||
"rpeak_tflops": 1321000.0,
|
||||
"cores": 9264128,
|
||||
"power_kw": 38698.0
|
||||
},
|
||||
{
|
||||
"id": "top500_3",
|
||||
"name": "Eagle",
|
||||
"latitude": 52.36,
|
||||
"longitude": 4.90,
|
||||
"country": "Netherlands",
|
||||
"city": "Amsterdam",
|
||||
"rank": 3,
|
||||
"rmax_tflops": 561200.0,
|
||||
"rpeak_tflops": 736000.0,
|
||||
"cores": 4620800,
|
||||
"power_kw": 12410.0
|
||||
},
|
||||
{
|
||||
"id": "top500_4",
|
||||
"name": "Fugaku",
|
||||
"latitude": 34.66,
|
||||
"longitude": 135.22,
|
||||
"country": "Japan",
|
||||
"city": "Kobe",
|
||||
"rank": 4,
|
||||
"rmax_tflops": 442010.0,
|
||||
"rpeak_tflops": 537212.0,
|
||||
"cores": 7630848,
|
||||
"power_kw": 29899.0
|
||||
},
|
||||
{
|
||||
"id": "top500_5",
|
||||
"name": "LUMI",
|
||||
"latitude": 65.01,
|
||||
"longitude": 25.46,
|
||||
"country": "Finland",
|
||||
"city": "Kajaani",
|
||||
"rank": 5,
|
||||
"rmax_tflops": 379700.0,
|
||||
"rpeak_tflops": 531000.0,
|
||||
"cores": 2748384,
|
||||
"power_kw": 6016.0
|
||||
},
|
||||
{
|
||||
"id": "top500_6",
|
||||
"name": "MareNostrum 5",
|
||||
"latitude": 41.39,
|
||||
"longitude": 2.11,
|
||||
"country": "Spain",
|
||||
"city": "Barcelona",
|
||||
"rank": 6,
|
||||
"rmax_tflops": 138200.0,
|
||||
"rpeak_tflops": 314000.0,
|
||||
"cores": 2764800,
|
||||
"power_kw": 10000.0
|
||||
},
|
||||
{
|
||||
"id": "top500_7",
|
||||
"name": "Summit",
|
||||
"latitude": 35.93,
|
||||
"longitude": -84.31,
|
||||
"country": "United States",
|
||||
"city": "Oak Ridge",
|
||||
"rank": 7,
|
||||
"rmax_tflops": 148600.0,
|
||||
"rpeak_tflops": 200795.0,
|
||||
"cores": 2414592,
|
||||
"power_kw": 10096.0
|
||||
},
|
||||
{
|
||||
"id": "top500_8",
|
||||
"name": "Tianhe-2A",
|
||||
"latitude": 23.13,
|
||||
"longitude": 113.27,
|
||||
"country": "China",
|
||||
"city": "Guangzhou",
|
||||
"rank": 8,
|
||||
"rmax_tflops": 100679.0,
|
||||
"rpeak_tflops": 100679.0,
|
||||
"cores": 4981760,
|
||||
"power_kw": 18482.0
|
||||
},
|
||||
{
|
||||
"id": "top500_9",
|
||||
"name": "Selene",
|
||||
"latitude": 40.82,
|
||||
"longitude": -74.17,
|
||||
"country": "United States",
|
||||
"city": "Santa Clara",
|
||||
"rank": 9,
|
||||
"rmax_tflops": 63460.0,
|
||||
"rpeak_tflops": 79200.0,
|
||||
"cores": 555520,
|
||||
"power_kw": 2646.0
|
||||
},
|
||||
{
|
||||
"id": "top500_10",
|
||||
"name": "Perlmutter",
|
||||
"latitude": 37.88,
|
||||
"longitude": -122.25,
|
||||
"country": "United States",
|
||||
"city": "Berkeley",
|
||||
"rank": 10,
|
||||
"rmax_tflops": 93750.0,
|
||||
"rpeak_tflops": 120000.0,
|
||||
"cores": 761856,
|
||||
"power_kw": 3060.0
|
||||
}
|
||||
]
|
||||
}
|
||||
53
ue_client/Source/PlanetClient/ComputePointActor.cpp
Normal file
53
ue_client/Source/PlanetClient/ComputePointActor.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "ComputePointActor.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
|
||||
AComputePointActor::AComputePointActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
SphereMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SphereMesh"));
|
||||
RootComponent = SphereMesh;
|
||||
|
||||
// Enable mouse-over events so the PlayerController can detect hover
|
||||
SphereMesh->bReceivesDecals = false;
|
||||
}
|
||||
|
||||
void AComputePointActor::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
SetActorScale3D(FVector(PointScale));
|
||||
ApplyMaterial();
|
||||
}
|
||||
|
||||
void AComputePointActor::SetPointData(const FComputePoint& Data)
|
||||
{
|
||||
PointData = Data;
|
||||
|
||||
// Optional: name the actor in the Outliner for easy debugging
|
||||
SetActorLabel(FString::Printf(TEXT("[%d] %s"), Data.Rank, *Data.Name));
|
||||
}
|
||||
|
||||
void AComputePointActor::SetVisualState(EPointVisualState NewState)
|
||||
{
|
||||
if (VisualState == NewState) return;
|
||||
VisualState = NewState;
|
||||
ApplyMaterial();
|
||||
}
|
||||
|
||||
void AComputePointActor::ApplyMaterial()
|
||||
{
|
||||
if (!SphereMesh) return;
|
||||
|
||||
UMaterialInterface* Mat = nullptr;
|
||||
switch (VisualState)
|
||||
{
|
||||
case EPointVisualState::Hovered: Mat = HoveredMaterial; break;
|
||||
case EPointVisualState::Selected: Mat = SelectedMaterial; break;
|
||||
default: Mat = NormalMaterial; break;
|
||||
}
|
||||
|
||||
if (Mat)
|
||||
{
|
||||
SphereMesh->SetMaterial(0, Mat);
|
||||
}
|
||||
}
|
||||
95
ue_client/Source/PlanetClient/ComputePointActor.h
Normal file
95
ue_client/Source/PlanetClient/ComputePointActor.h
Normal file
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "PlanetDataTypes.h"
|
||||
#include "ComputePointActor.generated.h"
|
||||
|
||||
class UStaticMeshComponent;
|
||||
class UBillboardComponent;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EPointVisualState : uint8
|
||||
{
|
||||
Normal UMETA(DisplayName="Normal"),
|
||||
Hovered UMETA(DisplayName="Hovered"),
|
||||
Selected UMETA(DisplayName="Selected"),
|
||||
};
|
||||
|
||||
/**
|
||||
* AComputePointActor
|
||||
*
|
||||
* Represents one supercomputer data point on the Cesium globe.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Create a Blueprint subclass: Content Browser → New Blueprint →
|
||||
* search "AComputePointActor" as parent class → name it BP_ComputePointActor
|
||||
* 2. In the Blueprint, assign a sphere mesh to SphereMesh (Engine/BasicShapes/Sphere)
|
||||
* 3. Assign a material to NormalMaterial / HoveredMaterial / SelectedMaterial
|
||||
* 4. Set this Blueprint class in APlanetDataManager → ComputePointClass
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class PLANETCLIENT_API AComputePointActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AComputePointActor();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Components
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// The visible sphere. Assign a sphere mesh in your Blueprint subclass.
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Planet|Components")
|
||||
UStaticMeshComponent* SphereMesh;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Materials (assign in Blueprint subclass)
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Visual")
|
||||
UMaterialInterface* NormalMaterial;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Visual")
|
||||
UMaterialInterface* HoveredMaterial;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Visual")
|
||||
UMaterialInterface* SelectedMaterial;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Point scale (world units). Tweak this in the Details Panel.
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Visual")
|
||||
float PointScale = 80000.f;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Data (set by APlanetDataManager after spawn)
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FComputePoint PointData;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
EPointVisualState VisualState = EPointVisualState::Normal;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Called by APlanetDataManager immediately after spawning.
|
||||
UFUNCTION(BlueprintCallable, Category="Planet")
|
||||
void SetPointData(const FComputePoint& Data);
|
||||
|
||||
// Called by PlanetPlayerController on hover/select.
|
||||
UFUNCTION(BlueprintCallable, Category="Planet")
|
||||
void SetVisualState(EPointVisualState NewState);
|
||||
|
||||
// Convenience: is this point currently selected?
|
||||
UFUNCTION(BlueprintPure, Category="Planet")
|
||||
bool IsSelected() const { return VisualState == EPointVisualState::Selected; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
void ApplyMaterial();
|
||||
};
|
||||
122
ue_client/Source/PlanetClient/GlobeInteractionComponent.cpp
Normal file
122
ue_client/Source/PlanetClient/GlobeInteractionComponent.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "GlobeInteractionComponent.h"
|
||||
#include "CesiumGeoreference.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
UGlobeInteractionComponent::UGlobeInteractionComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
}
|
||||
|
||||
void UGlobeInteractionComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
FindGeoreference();
|
||||
}
|
||||
|
||||
void UGlobeInteractionComponent::FindGeoreference()
|
||||
{
|
||||
Georeference = ACesiumGeoreference::GetDefaultGeoreference(GetWorld());
|
||||
if (!Georeference)
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("GlobeInteractionComponent: 场景中没有 CesiumGeoreference,请添加一个"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 旋转
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UGlobeInteractionComponent::RotateGlobe(float DeltaYaw, float DeltaPitch)
|
||||
{
|
||||
if (!Georeference) return;
|
||||
|
||||
// 累积惯性速度(本帧的输入叠加到惯性上)
|
||||
InertiaYaw += DeltaYaw * RotateSensitivity;
|
||||
InertiaPitch += DeltaPitch * RotateSensitivity;
|
||||
|
||||
// 立即应用(Tick 里再处理惯性衰减)
|
||||
double NewLon = Georeference->GetOriginLongitude() - InertiaYaw;
|
||||
double NewLat = FMath::Clamp(
|
||||
Georeference->GetOriginLatitude() + InertiaPitch,
|
||||
-85.0, 85.0
|
||||
);
|
||||
|
||||
Georeference->SetOriginLongitude(NewLon);
|
||||
Georeference->SetOriginLatitude(NewLat);
|
||||
|
||||
// 保持经度在 [-180, 180]
|
||||
if (NewLon > 180.0) Georeference->SetOriginLongitude(NewLon - 360.0);
|
||||
if (NewLon < -180.0) Georeference->SetOriginLongitude(NewLon + 360.0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 缩放
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UGlobeInteractionComponent::ZoomGlobe(float Value)
|
||||
{
|
||||
if (FMath::IsNearlyZero(Value)) return;
|
||||
|
||||
// Value > 0 = 拉近(高度减小),Value < 0 = 推远(高度增大)
|
||||
CurrentAltitudeMeters -= Value * ZoomSensitivity;
|
||||
CurrentAltitudeMeters = FMath::Clamp(CurrentAltitudeMeters,
|
||||
MinAltitudeMeters,
|
||||
MaxAltitudeMeters);
|
||||
ApplyAltitudeToCamera();
|
||||
}
|
||||
|
||||
void UGlobeInteractionComponent::ApplyAltitudeToCamera()
|
||||
{
|
||||
if (!Georeference) return;
|
||||
|
||||
// 把当前 Origin 经纬度 + 新高度转换为 Unreal 世界坐标,然后移动拥有者 Pawn
|
||||
AController* Controller = Cast<AController>(GetOwner());
|
||||
APawn* Pawn = Controller ? Controller->GetPawn() : Cast<APawn>(GetOwner());
|
||||
if (!Pawn) return;
|
||||
|
||||
double Lon = Georeference->GetOriginLongitude();
|
||||
double Lat = Georeference->GetOriginLatitude();
|
||||
|
||||
FVector NewPos = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(
|
||||
FVector(Lon, Lat, CurrentAltitudeMeters)
|
||||
);
|
||||
Pawn->SetActorLocation(NewPos);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 惯性 Tick
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UGlobeInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// 只有惯性速度足够大时才继续转动
|
||||
if (FMath::IsNearlyZero(InertiaYaw, 0.001f) &&
|
||||
FMath::IsNearlyZero(InertiaPitch, 0.001f)) return;
|
||||
|
||||
if (!Georeference) return;
|
||||
|
||||
double NewLon = Georeference->GetOriginLongitude() - InertiaYaw;
|
||||
double NewLat = FMath::Clamp(
|
||||
Georeference->GetOriginLatitude() + InertiaPitch,
|
||||
-85.0, 85.0
|
||||
);
|
||||
Georeference->SetOriginLongitude(NewLon);
|
||||
Georeference->SetOriginLatitude(NewLat);
|
||||
|
||||
// 惯性衰减
|
||||
InertiaYaw *= InertiaDamping;
|
||||
InertiaPitch *= InertiaDamping;
|
||||
}
|
||||
|
||||
void UGlobeInteractionComponent::StopInertia()
|
||||
{
|
||||
InertiaYaw = 0.f;
|
||||
InertiaPitch = 0.f;
|
||||
}
|
||||
96
ue_client/Source/PlanetClient/GlobeInteractionComponent.h
Normal file
96
ue_client/Source/PlanetClient/GlobeInteractionComponent.h
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GlobeInteractionComponent.generated.h"
|
||||
|
||||
class ACesiumGeoreference;
|
||||
|
||||
/**
|
||||
* UGlobeInteractionComponent
|
||||
*
|
||||
* 挂在 PlayerController 或 Pawn 上,处理地球旋转和缩放。
|
||||
*
|
||||
* 旋转原理:
|
||||
* 修改 CesiumGeoreference 的 OriginLongitude / OriginLatitude,
|
||||
* 相当于"移动地球"到新中心,视觉效果等同于旋转地球。
|
||||
*
|
||||
* 缩放原理:
|
||||
* 沿相机到地球中心方向移动相机,改变观察高度。
|
||||
*
|
||||
* 使用方式:
|
||||
* 在 PlanetPlayerController::BeginPlay 里 AddComponent,
|
||||
* 收到拖拽/滚轮输入时调用 RotateGlobe / ZoomGlobe。
|
||||
*/
|
||||
UCLASS(ClassGroup=(Planet), meta=(BlueprintSpawnableComponent), BlueprintType)
|
||||
class PLANETCLIENT_API UGlobeInteractionComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UGlobeInteractionComponent();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 可在 Details 面板调整的参数
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// 拖拽灵敏度:每像素对应的经纬度偏移量(度)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Globe")
|
||||
float RotateSensitivity = 0.15f;
|
||||
|
||||
// 缩放灵敏度:每格滚轮对应的高度变化(米)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Globe")
|
||||
float ZoomSensitivity = 200000.f;
|
||||
|
||||
// 最小观察高度(米),防止穿入地面
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Globe")
|
||||
double MinAltitudeMeters = 500000.0;
|
||||
|
||||
// 最大观察高度(米)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Globe")
|
||||
double MaxAltitudeMeters = 30000000.0;
|
||||
|
||||
// 旋转惯性平滑系数 (0=无惯性, 1=永不停)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Globe",
|
||||
meta=(ClampMin="0.0", ClampMax="0.99"))
|
||||
float InertiaDamping = 0.85f;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 公开 API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// 每帧由 PlayerController 调用,传入鼠标位移(像素)
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Globe")
|
||||
void RotateGlobe(float DeltaYaw, float DeltaPitch);
|
||||
|
||||
// 每帧由 PlayerController 调用,Value > 0 = 拉近,< 0 = 推远
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Globe")
|
||||
void ZoomGlobe(float Value);
|
||||
|
||||
// 立即停止惯性
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Globe")
|
||||
void StopInertia();
|
||||
|
||||
// 返回当前相机高度(米)
|
||||
UFUNCTION(BlueprintPure, Category="Planet|Globe")
|
||||
double GetCurrentAltitudeMeters() const { return CurrentAltitudeMeters; }
|
||||
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
ACesiumGeoreference* Georeference = nullptr;
|
||||
|
||||
// 惯性速度(经度/帧,纬度/帧)
|
||||
float InertiaYaw = 0.f;
|
||||
float InertiaPitch = 0.f;
|
||||
|
||||
// 当前相机高度(米),随 Zoom 更新
|
||||
double CurrentAltitudeMeters = 5000000.0;
|
||||
|
||||
void FindGeoreference();
|
||||
void ApplyAltitudeToCamera();
|
||||
};
|
||||
53
ue_client/Source/PlanetClient/InteractiveObjectBase.cpp
Normal file
53
ue_client/Source/PlanetClient/InteractiveObjectBase.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "InteractiveObjectBase.h"
|
||||
|
||||
AInteractiveObjectBase::AInteractiveObjectBase()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void AInteractiveObjectBase::SetInteractiveState(EInteractiveObjectState NewState)
|
||||
{
|
||||
if (CurrentState == NewState) return;
|
||||
if (CurrentState == EInteractiveObjectState::Disabled &&
|
||||
NewState != EInteractiveObjectState::Normal) return;
|
||||
|
||||
EInteractiveObjectState OldState = CurrentState;
|
||||
CurrentState = NewState;
|
||||
|
||||
switch (NewState)
|
||||
{
|
||||
case EInteractiveObjectState::Hovered: OnHovered(); break;
|
||||
case EInteractiveObjectState::Selected: OnSelected(); break;
|
||||
case EInteractiveObjectState::Normal:
|
||||
if (OldState == EInteractiveObjectState::Selected) OnDeselected();
|
||||
else OnRestored();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
OnStateChanged.Broadcast(NewState);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 默认实现(子类可重写)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
FText AInteractiveObjectBase::GetInfoTitle_Implementation() const
|
||||
{
|
||||
return FText::FromString(GetActorLabel());
|
||||
}
|
||||
|
||||
FText AInteractiveObjectBase::GetInfoBody_Implementation() const
|
||||
{
|
||||
return FText::FromString(TEXT(""));
|
||||
}
|
||||
|
||||
UTexture2D* AInteractiveObjectBase::GetInfoIcon_Implementation() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AInteractiveObjectBase::OnHovered_Implementation() {}
|
||||
void AInteractiveObjectBase::OnSelected_Implementation() {}
|
||||
void AInteractiveObjectBase::OnDeselected_Implementation(){}
|
||||
void AInteractiveObjectBase::OnRestored_Implementation() {}
|
||||
93
ue_client/Source/PlanetClient/InteractiveObjectBase.h
Normal file
93
ue_client/Source/PlanetClient/InteractiveObjectBase.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "InteractiveObjectBase.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EInteractiveObjectState : uint8
|
||||
{
|
||||
Normal UMETA(DisplayName="正常"),
|
||||
Hovered UMETA(DisplayName="悬停高亮"),
|
||||
Selected UMETA(DisplayName="已选中"),
|
||||
Disabled UMETA(DisplayName="不可交互"),
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnObjectStateChanged,
|
||||
EInteractiveObjectState, NewState);
|
||||
|
||||
/**
|
||||
* AInteractiveObjectBase
|
||||
*
|
||||
* 所有可交互 3D 物件的基类。
|
||||
* 当前场景中的可交互物件包括:
|
||||
* - AComputePointActor(超算数据点)—— 已实现
|
||||
* - 其他演示物件(TODO: 根据需求添加子类)
|
||||
*
|
||||
* 子类需要:
|
||||
* 1. 重写 OnHovered / OnSelected / OnDeselected(或响应 OnStateChanged 委托)
|
||||
* 2. 将根 Component 设置为可被射线检测(SetCollisionResponseToChannel)
|
||||
* 3. 可选:重写 GetInfoTitle / GetInfoBody 供信息卡使用
|
||||
*/
|
||||
UCLASS(Abstract, BlueprintType, Blueprintable)
|
||||
class PLANETCLIENT_API AInteractiveObjectBase : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AInteractiveObjectBase();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 状态变更委托
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Interactive")
|
||||
FOnObjectStateChanged OnStateChanged;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 状态 API(由 PlanetPlayerController 调用)
|
||||
// -----------------------------------------------------------------------
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Interactive")
|
||||
void SetInteractiveState(EInteractiveObjectState NewState);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Planet|Interactive")
|
||||
EInteractiveObjectState GetInteractiveState() const { return CurrentState; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Planet|Interactive")
|
||||
bool IsSelected() const { return CurrentState == EInteractiveObjectState::Selected; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Planet|Interactive")
|
||||
bool IsHovered() const { return CurrentState == EInteractiveObjectState::Hovered; }
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 信息卡内容(子类可重写提供具体文本)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// 信息卡标题,默认返回 Actor 名
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="Planet|Interactive")
|
||||
FText GetInfoTitle() const;
|
||||
|
||||
// 信息卡正文,返回富文本格式的详情
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="Planet|Interactive")
|
||||
FText GetInfoBody() const;
|
||||
|
||||
// 信息卡图标(可选,返回空则不显示图标)
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="Planet|Interactive")
|
||||
UTexture2D* GetInfoIcon() const;
|
||||
|
||||
protected:
|
||||
// 子类重写这三个函数来处理视觉状态变化
|
||||
UFUNCTION(BlueprintNativeEvent, Category="Planet|Interactive")
|
||||
void OnHovered();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category="Planet|Interactive")
|
||||
void OnSelected();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category="Planet|Interactive")
|
||||
void OnDeselected();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category="Planet|Interactive")
|
||||
void OnRestored(); // 从任何状态回到 Normal
|
||||
|
||||
private:
|
||||
EInteractiveObjectState CurrentState = EInteractiveObjectState::Normal;
|
||||
};
|
||||
150
ue_client/Source/PlanetClient/MotionCaptureInterface.h
Normal file
150
ue_client/Source/PlanetClient/MotionCaptureInterface.h
Normal file
@@ -0,0 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "MotionCaptureInterface.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 动捕事件 —— 供应商中间件触发这些事件,UE5 侧响应
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 手势类型枚举
|
||||
*
|
||||
* TODO(等动捕供应商确认手势集合后补充):
|
||||
* 当前列出的是推测的基本手势,以实际交付为准。
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EMotionGesture : uint8
|
||||
{
|
||||
None UMETA(DisplayName="无"),
|
||||
SwipeLeft UMETA(DisplayName="向左划"), // 地球向左转
|
||||
SwipeRight UMETA(DisplayName="向右划"), // 地球向右转
|
||||
SwipeUp UMETA(DisplayName="向上划"), // 地球向上转
|
||||
SwipeDown UMETA(DisplayName="向下划"), // 地球向下转
|
||||
PinchIn UMETA(DisplayName="捏合(缩小)"),// 缩放
|
||||
PinchOut UMETA(DisplayName="展开(放大)"),// 缩放
|
||||
Point UMETA(DisplayName="指向"), // 悬停选中
|
||||
Confirm UMETA(DisplayName="确认"), // 点击选中
|
||||
Dismiss UMETA(DisplayName="挥手取消"), // 关闭信息卡
|
||||
};
|
||||
|
||||
// 动捕系统触发某个离散手势时广播
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGestureDetected, EMotionGesture, Gesture);
|
||||
|
||||
// 动捕系统持续输出指向方向时广播(用于悬停高亮)
|
||||
// Direction: 归一化方向向量,由动捕系统解算后传入
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPointingDirectionChanged, FVector, Direction);
|
||||
|
||||
// 动捕系统输出旋转增量时广播(用于连续拖拽旋转地球)
|
||||
// DeltaYaw/DeltaPitch: 单帧内的角度增量(度)
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnRotateDelta, float, DeltaYaw, float, DeltaPitch);
|
||||
|
||||
// 动捕系统输出缩放增量时广播
|
||||
// DeltaScale: > 0 放大,< 0 缩小
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnZoomDelta, float, DeltaScale);
|
||||
|
||||
|
||||
/**
|
||||
* UMotionCaptureReceiver
|
||||
*
|
||||
* 动捕接收器基类。根据供应商协议,创建对应的子类实现:
|
||||
*
|
||||
* - LiveLink 协议 → UMotionCaptureReceiverLiveLink (TODO)
|
||||
* - OSC/UDP 协议 → UMotionCaptureReceiverOSC (TODO)
|
||||
*
|
||||
* 子类负责接收数据并调用 BroadcastXxx() 方法,
|
||||
* PlanetPlayerController 订阅这些委托即可,不关心底层协议。
|
||||
*
|
||||
* TODO(等动捕供应商回复后):
|
||||
* 1. 确认协议(Live Link / OSC / 私有 SDK)
|
||||
* 2. 在对应子类里实现连接和数据接收
|
||||
* 3. 把子类拖进场景,在 PlanetDataManager / PlayerController 里引用它
|
||||
*/
|
||||
UCLASS(Abstract, BlueprintType, Blueprintable)
|
||||
class PLANETCLIENT_API UMotionCaptureReceiver : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// -----------------------------------------------------------------------
|
||||
// 外部订阅这些委托
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|MotionCapture")
|
||||
FOnGestureDetected OnGestureDetected;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|MotionCapture")
|
||||
FOnPointingDirectionChanged OnPointingDirectionChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|MotionCapture")
|
||||
FOnRotateDelta OnRotateDelta;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|MotionCapture")
|
||||
FOnZoomDelta OnZoomDelta;
|
||||
|
||||
// 连接到动捕中间件(子类实现)
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|MotionCapture")
|
||||
virtual void Connect() {}
|
||||
|
||||
// 断开连接(子类实现)
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|MotionCapture")
|
||||
virtual void Disconnect() {}
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Planet|MotionCapture")
|
||||
virtual bool IsConnected() const { return false; }
|
||||
|
||||
protected:
|
||||
// 子类解析到数据后调用这些方法广播
|
||||
void BroadcastGesture(EMotionGesture Gesture)
|
||||
{
|
||||
OnGestureDetected.Broadcast(Gesture);
|
||||
}
|
||||
|
||||
void BroadcastPointing(const FVector& Direction)
|
||||
{
|
||||
OnPointingDirectionChanged.Broadcast(Direction);
|
||||
}
|
||||
|
||||
void BroadcastRotateDelta(float DeltaYaw, float DeltaPitch)
|
||||
{
|
||||
OnRotateDelta.Broadcast(DeltaYaw, DeltaPitch);
|
||||
}
|
||||
|
||||
void BroadcastZoomDelta(float DeltaScale)
|
||||
{
|
||||
OnZoomDelta.Broadcast(DeltaScale);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 动作映射配置
|
||||
//
|
||||
// 把动捕手势映射到具体的 UE 操作,在 Details 面板里可以改。
|
||||
// 这样不用改代码就能重新映射手势。
|
||||
// ---------------------------------------------------------------------------
|
||||
USTRUCT(BlueprintType)
|
||||
struct FMotionActionMapping
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// 触发"旋转地球"的手势(持续输出增量)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|MotionCapture")
|
||||
EMotionGesture RotateGlobe = EMotionGesture::None; // TODO: 填入供应商手势名
|
||||
|
||||
// 触发"放大"的手势
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|MotionCapture")
|
||||
EMotionGesture ZoomIn = EMotionGesture::PinchOut;
|
||||
|
||||
// 触发"缩小"的手势
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|MotionCapture")
|
||||
EMotionGesture ZoomOut = EMotionGesture::PinchIn;
|
||||
|
||||
// 触发"选中数据点"的手势
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|MotionCapture")
|
||||
EMotionGesture SelectPoint = EMotionGesture::Confirm;
|
||||
|
||||
// 触发"关闭信息卡"的手势
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|MotionCapture")
|
||||
EMotionGesture DismissInfoCard = EMotionGesture::Dismiss;
|
||||
};
|
||||
33
ue_client/Source/PlanetClient/PlanetClient.Build.cs
Normal file
33
ue_client/Source/PlanetClient/PlanetClient.Build.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class PlanetClient : ModuleRules
|
||||
{
|
||||
public PlanetClient(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"InputCore",
|
||||
"HTTP",
|
||||
"Json",
|
||||
"JsonUtilities",
|
||||
"CesiumRuntime",
|
||||
"UMG",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"RenderCore", // StereoRenderingManager 用到
|
||||
"RHI", // 渲染硬件接口
|
||||
// TODO: 动捕协议确认后按需添加:
|
||||
// "LiveLink", // Live Link 协议
|
||||
// "LiveLinkInterface",
|
||||
// "Networking", // OSC/UDP 协议
|
||||
// "Sockets",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { });
|
||||
}
|
||||
}
|
||||
4
ue_client/Source/PlanetClient/PlanetClient.cpp
Normal file
4
ue_client/Source/PlanetClient/PlanetClient.cpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#include "PlanetClient.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, PlanetClient, "PlanetClient");
|
||||
3
ue_client/Source/PlanetClient/PlanetClient.h
Normal file
3
ue_client/Source/PlanetClient/PlanetClient.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
220
ue_client/Source/PlanetClient/PlanetDataManager.cpp
Normal file
220
ue_client/Source/PlanetClient/PlanetDataManager.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
#include "PlanetDataManager.h"
|
||||
#include "ComputePointActor.h"
|
||||
|
||||
#include "HttpModule.h"
|
||||
#include "Interfaces/IHttpResponse.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
// Cesium coordinate conversion
|
||||
#include "CesiumGeoreference.h"
|
||||
|
||||
APlanetDataManager::APlanetDataManager()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void APlanetDataManager::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Build default mock path if not overridden
|
||||
if (MockDataPath.IsEmpty())
|
||||
{
|
||||
MockDataPath = FPaths::ProjectContentDir() / TEXT("Data/mock_compute_points.json");
|
||||
}
|
||||
|
||||
FetchAllData();
|
||||
}
|
||||
|
||||
void APlanetDataManager::FetchAllData()
|
||||
{
|
||||
// Clear previous actors
|
||||
for (AComputePointActor* Point : SpawnedPoints)
|
||||
{
|
||||
if (IsValid(Point)) Point->Destroy();
|
||||
}
|
||||
SpawnedPoints.Empty();
|
||||
|
||||
if (bUseLocalMockData)
|
||||
{
|
||||
LoadMockComputePoints();
|
||||
}
|
||||
else
|
||||
{
|
||||
FetchComputePoints();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: local JSON file
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetDataManager::LoadMockComputePoints()
|
||||
{
|
||||
FString JsonStr;
|
||||
if (!FFileHelper::LoadFileToString(JsonStr, *MockDataPath))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: cannot read mock file: %s"), *MockDataPath);
|
||||
return;
|
||||
}
|
||||
TArray<FComputePoint> Points = ParseComputePointsJson(JsonStr);
|
||||
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: loaded %d points from mock file"), Points.Num());
|
||||
SpawnComputePoints(Points);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: HTTP request
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetDataManager::FetchComputePoints()
|
||||
{
|
||||
const FString Url = BackendBaseUrl + TEXT("/api/v1/ue/compute-points");
|
||||
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Req = FHttpModule::Get().CreateRequest();
|
||||
Req->SetURL(Url);
|
||||
Req->SetVerb(TEXT("GET"));
|
||||
Req->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
|
||||
Req->OnProcessRequestComplete().BindUObject(
|
||||
this, &APlanetDataManager::OnComputePointsResponse);
|
||||
Req->ProcessRequest();
|
||||
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: GET %s"), *Url);
|
||||
}
|
||||
|
||||
void APlanetDataManager::OnComputePointsResponse(FHttpRequestPtr Request,
|
||||
FHttpResponsePtr Response,
|
||||
bool bSuccess)
|
||||
{
|
||||
if (!bSuccess || !Response.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("PlanetDataManager: HTTP request failed. Is the backend running at %s?"),
|
||||
*BackendBaseUrl);
|
||||
return;
|
||||
}
|
||||
if (Response->GetResponseCode() != 200)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: HTTP %d from %s"),
|
||||
Response->GetResponseCode(), *Request->GetURL());
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FComputePoint> Points = ParseComputePointsJson(Response->GetContentAsString());
|
||||
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: received %d compute points"), Points.Num());
|
||||
SpawnComputePoints(Points);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TArray<FComputePoint> APlanetDataManager::ParseComputePointsJson(const FString& JsonStr)
|
||||
{
|
||||
TArray<FComputePoint> Result;
|
||||
|
||||
TSharedPtr<FJsonObject> Root;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonStr);
|
||||
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: JSON parse failed"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* Items;
|
||||
if (!Root->TryGetArrayField(TEXT("items"), Items))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: no 'items' array in JSON"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& Val : *Items)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* ObjPtr;
|
||||
if (!Val->TryGetObject(ObjPtr)) continue;
|
||||
const TSharedPtr<FJsonObject>& Obj = *ObjPtr;
|
||||
|
||||
FComputePoint Pt;
|
||||
Obj->TryGetStringField(TEXT("id"), Pt.Id);
|
||||
Obj->TryGetStringField(TEXT("name"), Pt.Name);
|
||||
Obj->TryGetStringField(TEXT("country"), Pt.Country);
|
||||
Obj->TryGetStringField(TEXT("city"), Pt.City);
|
||||
|
||||
double Lat, Lon;
|
||||
if (!Obj->TryGetNumberField(TEXT("latitude"), Lat)) continue;
|
||||
if (!Obj->TryGetNumberField(TEXT("longitude"), Lon)) continue;
|
||||
Pt.Latitude = (float)Lat;
|
||||
Pt.Longitude = (float)Lon;
|
||||
|
||||
int32 Rank;
|
||||
if (Obj->TryGetNumberField(TEXT("rank"), Rank)) Pt.Rank = Rank;
|
||||
|
||||
double Rmax;
|
||||
if (Obj->TryGetNumberField(TEXT("rmax_tflops"), Rmax)) Pt.RmaxTFlops = (float)Rmax;
|
||||
double Rpeak;
|
||||
if (Obj->TryGetNumberField(TEXT("rpeak_tflops"), Rpeak)) Pt.RpeakTFlops = (float)Rpeak;
|
||||
int32 Cores;
|
||||
if (Obj->TryGetNumberField(TEXT("cores"), Cores)) Pt.Cores = Cores;
|
||||
double Power;
|
||||
if (Obj->TryGetNumberField(TEXT("power_kw"), Power)) Pt.PowerKw = (float)Power;
|
||||
|
||||
Result.Add(Pt);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spawn actors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetDataManager::SpawnComputePoints(const TArray<FComputePoint>& Points)
|
||||
{
|
||||
if (!ComputePointClass)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("PlanetDataManager: ComputePointClass not set — set it in the Details Panel"));
|
||||
return;
|
||||
}
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) return;
|
||||
|
||||
// Find the CesiumGeoreference in the level
|
||||
ACesiumGeoreference* Georeference = ACesiumGeoreference::GetDefaultGeoreference(World);
|
||||
if (!Georeference)
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("PlanetDataManager: no CesiumGeoreference in level. Add a CesiumGeoreference actor."));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const FComputePoint& Pt : Points)
|
||||
{
|
||||
// Convert geographic coordinates to Unreal world coordinates
|
||||
// Altitude is PointAltitudeMeters above sea level
|
||||
FVector WorldPos = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(
|
||||
FVector(Pt.Longitude, Pt.Latitude, PointAltitudeMeters)
|
||||
);
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.SpawnCollisionHandlingOverride =
|
||||
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
AComputePointActor* Actor = World->SpawnActor<AComputePointActor>(
|
||||
ComputePointClass, WorldPos, FRotator::ZeroRotator, Params);
|
||||
|
||||
if (Actor)
|
||||
{
|
||||
Actor->SetPointData(Pt);
|
||||
SpawnedPoints.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
OnComputePointsLoaded.Broadcast(SpawnedPoints.Num());
|
||||
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: spawned %d compute point actors"),
|
||||
SpawnedPoints.Num());
|
||||
}
|
||||
97
ue_client/Source/PlanetClient/PlanetDataManager.h
Normal file
97
ue_client/Source/PlanetClient/PlanetDataManager.h
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Interfaces/IHttpRequest.h"
|
||||
#include "PlanetDataTypes.h"
|
||||
#include "PlanetDataManager.generated.h"
|
||||
|
||||
class AComputePointActor;
|
||||
|
||||
// Fired once all compute points have been spawned into the world
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnComputePointsLoaded, int32, Count);
|
||||
|
||||
/**
|
||||
* APlanetDataManager
|
||||
*
|
||||
* Place one of these in your level. On BeginPlay it fetches data from the
|
||||
* Planet backend and spawns AComputePointActor instances at the correct
|
||||
* Cesium globe positions.
|
||||
*
|
||||
* Phase A (local): Set bUseLocalMockData = true and point MockDataPath to
|
||||
* Content/Data/mock_compute_points.json.
|
||||
* Phase B (live): Set bUseLocalMockData = false and fill BackendBaseUrl.
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class PLANETCLIENT_API APlanetDataManager : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APlanetDataManager();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Inspector-editable properties
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Base URL of the Planet backend, e.g. "http://localhost:8000"
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Config")
|
||||
FString BackendBaseUrl = TEXT("http://localhost:8000");
|
||||
|
||||
// When true, loads mock_compute_points.json from disk instead of the API.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Config")
|
||||
bool bUseLocalMockData = true;
|
||||
|
||||
// Absolute path to the mock JSON file (Phase A).
|
||||
// Default points to <ProjectDir>/Content/Data/mock_compute_points.json
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Config",
|
||||
meta=(EditCondition="bUseLocalMockData"))
|
||||
FString MockDataPath;
|
||||
|
||||
// The Blueprint subclass of AComputePointActor to spawn.
|
||||
// Set this in the Details Panel to your BP_ComputePointActor.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Config")
|
||||
TSubclassOf<AComputePointActor> ComputePointClass;
|
||||
|
||||
// Height above sea level (metres) at which to place data points.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Config")
|
||||
double PointAltitudeMeters = 50000.0;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Events
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Events")
|
||||
FOnComputePointsLoaded OnComputePointsLoaded;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Call this to re-fetch and rebuild all points.
|
||||
UFUNCTION(BlueprintCallable, Category="Planet")
|
||||
void FetchAllData();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Planet")
|
||||
TArray<AComputePointActor*> GetAllComputePoints() const { return SpawnedPoints; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
TArray<AComputePointActor*> SpawnedPoints;
|
||||
|
||||
// HTTP handlers
|
||||
void FetchComputePoints();
|
||||
void OnComputePointsResponse(FHttpRequestPtr Request,
|
||||
FHttpResponsePtr Response,
|
||||
bool bSuccess);
|
||||
|
||||
// Local mock data loader (Phase A)
|
||||
void LoadMockComputePoints();
|
||||
|
||||
// Shared spawn logic
|
||||
void SpawnComputePoints(const TArray<FComputePoint>& Points);
|
||||
|
||||
// JSON → FComputePoint array
|
||||
static TArray<FComputePoint> ParseComputePointsJson(const FString& JsonStr);
|
||||
};
|
||||
108
ue_client/Source/PlanetClient/PlanetDataTypes.h
Normal file
108
ue_client/Source/PlanetClient/PlanetDataTypes.h
Normal file
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "PlanetDataTypes.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FComputePoint — corresponds to /api/v1/ue/compute-points items
|
||||
// ---------------------------------------------------------------------------
|
||||
USTRUCT(BlueprintType)
|
||||
struct FComputePoint
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// Unique string id (e.g. "top500_42")
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Id;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Name;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float Latitude = 0.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float Longitude = 0.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Country;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString City;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
int32 Rank = 0;
|
||||
|
||||
// Rmax in TFlops. 0 means not available.
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float RmaxTFlops = 0.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float RpeakTFlops = 0.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
int32 Cores = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float PowerKw = 0.f;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FLandingPoint — corresponds to /api/v1/ue/landing-points items
|
||||
// ---------------------------------------------------------------------------
|
||||
USTRUCT(BlueprintType)
|
||||
struct FLandingPoint
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Id;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Name;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float Latitude = 0.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float Longitude = 0.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Country;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString City;
|
||||
|
||||
// Names of cables that pass through this landing point
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
TArray<FString> CableNames;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FCableSegment / FCable — corresponds to /api/v1/ue/cables items
|
||||
// ---------------------------------------------------------------------------
|
||||
USTRUCT(BlueprintType)
|
||||
struct FCable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Id;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString CableId; // slug, e.g. "flag"
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Name;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
FString Status;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
float LengthKm = 0.f;
|
||||
|
||||
// Each inner TArray is one segment: ordered [lon, lat] pairs
|
||||
// Stored as FVector2D(lon, lat) per point
|
||||
UPROPERTY(BlueprintReadOnly, Category="Planet|Data")
|
||||
TArray<TArray<FVector2D>> Segments;
|
||||
};
|
||||
28
ue_client/Source/PlanetClient/PlanetGameMode.cpp
Normal file
28
ue_client/Source/PlanetClient/PlanetGameMode.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "PlanetGameMode.h"
|
||||
#include "PlanetPlayerController.h"
|
||||
#include "StereoRenderingManager.h"
|
||||
#include "GameFramework/SpectatorPawn.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
APlanetGameMode::APlanetGameMode()
|
||||
{
|
||||
PlayerControllerClass = APlanetPlayerController::StaticClass();
|
||||
// 真实运行时在 World Settings 里改为 CesiumDynamicPawn
|
||||
DefaultPawnClass = ASpectatorPawn::StaticClass();
|
||||
}
|
||||
|
||||
void APlanetGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// 自动在场景里生成 StereoRenderingManager(如果场景里没有的话)
|
||||
TArray<AActor*> Found;
|
||||
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AStereoRenderingManager::StaticClass(), Found);
|
||||
if (Found.Num() == 0)
|
||||
{
|
||||
GetWorld()->SpawnActor<AStereoRenderingManager>(
|
||||
AStereoRenderingManager::StaticClass(),
|
||||
FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
UE_LOG(LogTemp, Log, TEXT("PlanetGameMode: 已自动生成 StereoRenderingManager"));
|
||||
}
|
||||
}
|
||||
27
ue_client/Source/PlanetClient/PlanetGameMode.h
Normal file
27
ue_client/Source/PlanetClient/PlanetGameMode.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "PlanetGameMode.generated.h"
|
||||
|
||||
/**
|
||||
* APlanetGameMode
|
||||
*
|
||||
* 场景入口。BeginPlay 时自动生成 StereoRenderingManager。
|
||||
*
|
||||
* 编辑器操作(步骤见 ue_client_setup_guide.md):
|
||||
* 1. World Settings → Game Mode Override → PlanetGameMode
|
||||
* 2. World Settings → Default Pawn Class → CesiumDynamicPawn
|
||||
*/
|
||||
UCLASS()
|
||||
class PLANETCLIENT_API APlanetGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APlanetGameMode();
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
};
|
||||
246
ue_client/Source/PlanetClient/PlanetPlayerController.cpp
Normal file
246
ue_client/Source/PlanetClient/PlanetPlayerController.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
#include "PlanetPlayerController.h"
|
||||
#include "GlobeInteractionComponent.h"
|
||||
#include "InteractiveObjectBase.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
|
||||
APlanetPlayerController::APlanetPlayerController()
|
||||
{
|
||||
bShowMouseCursor = true;
|
||||
bEnableClickEvents = true;
|
||||
bEnableMouseOverEvents = true;
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
GlobeInteraction = CreateDefaultSubobject<UGlobeInteractionComponent>(
|
||||
TEXT("GlobeInteraction"));
|
||||
}
|
||||
|
||||
void APlanetPlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
SetInputMode(FInputModeGameAndUI());
|
||||
|
||||
// TODO(供应商回复后取消注释):
|
||||
// BindMotionCaptureEvents();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 输入绑定
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetPlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
|
||||
InputComponent->BindAction("SelectPoint", IE_Pressed, this,
|
||||
&APlanetPlayerController::OnLeftMouseDown);
|
||||
InputComponent->BindAction("SelectPoint", IE_Released, this,
|
||||
&APlanetPlayerController::OnLeftMouseUp);
|
||||
InputComponent->BindAction("Escape", IE_Pressed, this,
|
||||
&APlanetPlayerController::OnEscape);
|
||||
|
||||
InputComponent->BindAxis("RotateYaw", this, &APlanetPlayerController::OnMouseX);
|
||||
InputComponent->BindAxis("RotatePitch", this, &APlanetPlayerController::OnMouseY);
|
||||
InputComponent->BindAxis("ZoomCamera", this, &APlanetPlayerController::OnZoom);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tick:悬停检测
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetPlayerController::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
if (!bIsDragging) UpdateHover();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 鼠标输入
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetPlayerController::OnLeftMouseDown()
|
||||
{
|
||||
bLeftMouseDown = true;
|
||||
bIsDragging = false;
|
||||
|
||||
float X, Y;
|
||||
GetMousePosition(X, Y);
|
||||
MouseDownPosition = FVector2D(X, Y);
|
||||
|
||||
// 停止惯性,准备接管控制
|
||||
if (GlobeInteraction) GlobeInteraction->StopInertia();
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnLeftMouseUp()
|
||||
{
|
||||
if (!bIsDragging)
|
||||
{
|
||||
// 没有拖拽 → 判定为点击
|
||||
AInteractiveObjectBase* Clicked = GetObjectUnderCursor();
|
||||
if (Clicked && Clicked != SelectedObject)
|
||||
{
|
||||
// 取消上一个选中
|
||||
DeselectCurrent();
|
||||
// 选中新物件
|
||||
SelectedObject = Clicked;
|
||||
SelectedObject->SetInteractiveState(EInteractiveObjectState::Selected);
|
||||
OnObjectSelected.Broadcast(SelectedObject);
|
||||
}
|
||||
else if (!Clicked)
|
||||
{
|
||||
// 点了空白处 → 取消选中
|
||||
DeselectCurrent();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 点了已选中的物件 → 取消选中
|
||||
DeselectCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
bLeftMouseDown = false;
|
||||
bIsDragging = false;
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnMouseX(float Value)
|
||||
{
|
||||
if (!bLeftMouseDown || FMath::IsNearlyZero(Value)) return;
|
||||
|
||||
// 判断是否超过拖拽阈值
|
||||
if (!bIsDragging)
|
||||
{
|
||||
float X, Y;
|
||||
GetMousePosition(X, Y);
|
||||
float Dist = FVector2D::Distance(FVector2D(X, Y), MouseDownPosition);
|
||||
if (Dist < DragThresholdPixels) return;
|
||||
bIsDragging = true;
|
||||
}
|
||||
|
||||
if (GlobeInteraction) GlobeInteraction->RotateGlobe(Value * MouseDragSensitivity, 0.f);
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnMouseY(float Value)
|
||||
{
|
||||
if (!bLeftMouseDown || FMath::IsNearlyZero(Value)) return;
|
||||
if (!bIsDragging) return;
|
||||
|
||||
if (GlobeInteraction) GlobeInteraction->RotateGlobe(0.f, Value * MouseDragSensitivity);
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnZoom(float Value)
|
||||
{
|
||||
if (FMath::IsNearlyZero(Value)) return;
|
||||
if (GlobeInteraction) GlobeInteraction->ZoomGlobe(Value);
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnEscape()
|
||||
{
|
||||
DeselectCurrent();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 悬停
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetPlayerController::UpdateHover()
|
||||
{
|
||||
AInteractiveObjectBase* UnderCursor = GetObjectUnderCursor();
|
||||
|
||||
if (HoveredObject && HoveredObject != UnderCursor && HoveredObject != SelectedObject)
|
||||
{
|
||||
HoveredObject->SetInteractiveState(EInteractiveObjectState::Normal);
|
||||
}
|
||||
|
||||
HoveredObject = UnderCursor;
|
||||
|
||||
if (HoveredObject && HoveredObject != SelectedObject)
|
||||
{
|
||||
HoveredObject->SetInteractiveState(EInteractiveObjectState::Hovered);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 取消选中
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetPlayerController::DeselectCurrent()
|
||||
{
|
||||
if (!SelectedObject) return;
|
||||
SelectedObject->SetInteractiveState(EInteractiveObjectState::Normal);
|
||||
SelectedObject = nullptr;
|
||||
OnObjectDeselected.Broadcast();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 射线检测
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
AInteractiveObjectBase* APlanetPlayerController::GetObjectUnderCursor() const
|
||||
{
|
||||
FHitResult Hit;
|
||||
bool bHit = GetHitResultUnderCursorByChannel(
|
||||
UEngineTypes::ConvertToTraceType(ECC_Visibility), true, Hit);
|
||||
if (!bHit) return nullptr;
|
||||
return Cast<AInteractiveObjectBase>(Hit.GetActor());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 动捕事件(TODO: 供应商回复后实现)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void APlanetPlayerController::BindMotionCaptureEvents()
|
||||
{
|
||||
if (!MotionCaptureReceiver)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("PlayerController: MotionCaptureReceiver 未设置,动捕功能禁用"));
|
||||
return;
|
||||
}
|
||||
|
||||
MotionCaptureReceiver->OnGestureDetected.AddDynamic(
|
||||
this, &APlanetPlayerController::OnMotionGesture);
|
||||
MotionCaptureReceiver->OnRotateDelta.AddDynamic(
|
||||
this, &APlanetPlayerController::OnMotionRotate);
|
||||
MotionCaptureReceiver->OnZoomDelta.AddDynamic(
|
||||
this, &APlanetPlayerController::OnMotionZoom);
|
||||
|
||||
MotionCaptureReceiver->Connect();
|
||||
UE_LOG(LogTemp, Log, TEXT("PlayerController: 动捕事件已绑定"));
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnMotionGesture(EMotionGesture Gesture)
|
||||
{
|
||||
if (Gesture == MotionActionMapping.SelectPoint)
|
||||
{
|
||||
// 手势确认 → 等同于点击当前悬停的物件
|
||||
if (HoveredObject)
|
||||
{
|
||||
DeselectCurrent();
|
||||
SelectedObject = HoveredObject;
|
||||
SelectedObject->SetInteractiveState(EInteractiveObjectState::Selected);
|
||||
OnObjectSelected.Broadcast(SelectedObject);
|
||||
}
|
||||
}
|
||||
else if (Gesture == MotionActionMapping.DismissInfoCard)
|
||||
{
|
||||
DeselectCurrent();
|
||||
}
|
||||
else if (Gesture == MotionActionMapping.ZoomIn)
|
||||
{
|
||||
if (GlobeInteraction) GlobeInteraction->ZoomGlobe(1.f);
|
||||
}
|
||||
else if (Gesture == MotionActionMapping.ZoomOut)
|
||||
{
|
||||
if (GlobeInteraction) GlobeInteraction->ZoomGlobe(-1.f);
|
||||
}
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnMotionRotate(float DeltaYaw, float DeltaPitch)
|
||||
{
|
||||
if (GlobeInteraction) GlobeInteraction->RotateGlobe(DeltaYaw, DeltaPitch);
|
||||
}
|
||||
|
||||
void APlanetPlayerController::OnMotionZoom(float DeltaScale)
|
||||
{
|
||||
if (GlobeInteraction) GlobeInteraction->ZoomGlobe(DeltaScale);
|
||||
}
|
||||
117
ue_client/Source/PlanetClient/PlanetPlayerController.h
Normal file
117
ue_client/Source/PlanetClient/PlanetPlayerController.h
Normal file
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "MotionCaptureInterface.h"
|
||||
#include "PlanetPlayerController.generated.h"
|
||||
|
||||
class AInteractiveObjectBase;
|
||||
class UGlobeInteractionComponent;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnObjectSelected, AInteractiveObjectBase*, Object);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnObjectDeselected);
|
||||
|
||||
/**
|
||||
* APlanetPlayerController
|
||||
*
|
||||
* 统一处理两路输入:
|
||||
* 1. 鼠标/键盘(始终可用,调试和演示备用)
|
||||
* 2. 动捕手势(TODO: 供应商协议确认后接入)
|
||||
*
|
||||
* 鼠标操作:
|
||||
* 左键拖拽 → 旋转地球
|
||||
* 滚轮 → 缩放
|
||||
* 悬停 → 高亮物件
|
||||
* 左键单击 → 选中物件 / 取消选中
|
||||
* ESC → 取消选中
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class PLANETCLIENT_API APlanetPlayerController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APlanetPlayerController();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 组件
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Planet|Components")
|
||||
UGlobeInteractionComponent* GlobeInteraction;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 动捕配置(TODO: 供应商回复后配置)
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|MotionCapture")
|
||||
FMotionActionMapping MotionActionMapping;
|
||||
|
||||
// 动捕接收器实例(TODO: 确认协议后指向具体子类实例)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|MotionCapture")
|
||||
UMotionCaptureReceiver* MotionCaptureReceiver = nullptr;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 鼠标灵敏度
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Input")
|
||||
float MouseDragSensitivity = 0.3f;
|
||||
|
||||
// 判定为"拖拽"而非"点击"的最小移动距离(像素)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Input")
|
||||
float DragThresholdPixels = 5.f;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 事件
|
||||
// -----------------------------------------------------------------------
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Events")
|
||||
FOnObjectSelected OnObjectSelected;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Events")
|
||||
FOnObjectDeselected OnObjectDeselected;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 公开 API
|
||||
// -----------------------------------------------------------------------
|
||||
UFUNCTION(BlueprintCallable, Category="Planet")
|
||||
void DeselectCurrent();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Planet")
|
||||
AInteractiveObjectBase* GetSelectedObject() const { return SelectedObject; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual void SetupInputComponent() override;
|
||||
|
||||
private:
|
||||
// 鼠标拖拽状态
|
||||
bool bLeftMouseDown = false;
|
||||
bool bIsDragging = false;
|
||||
FVector2D MouseDownPosition = FVector2D::ZeroVector;
|
||||
|
||||
// 交互状态
|
||||
AInteractiveObjectBase* SelectedObject = nullptr;
|
||||
AInteractiveObjectBase* HoveredObject = nullptr;
|
||||
|
||||
// 输入回调
|
||||
void OnLeftMouseDown();
|
||||
void OnLeftMouseUp();
|
||||
void OnMouseX(float Value);
|
||||
void OnMouseY(float Value);
|
||||
void OnZoom(float Value);
|
||||
void OnEscape();
|
||||
|
||||
// 悬停检测
|
||||
void UpdateHover();
|
||||
|
||||
// 射线检测
|
||||
AInteractiveObjectBase* GetObjectUnderCursor() const;
|
||||
|
||||
// 动捕事件绑定(TODO: 供应商回复后在 BeginPlay 里绑定接收器)
|
||||
UFUNCTION()
|
||||
void OnMotionGesture(EMotionGesture Gesture);
|
||||
UFUNCTION()
|
||||
void OnMotionRotate(float DeltaYaw, float DeltaPitch);
|
||||
UFUNCTION()
|
||||
void OnMotionZoom(float DeltaScale);
|
||||
void BindMotionCaptureEvents();
|
||||
};
|
||||
131
ue_client/Source/PlanetClient/StereoRenderingManager.cpp
Normal file
131
ue_client/Source/PlanetClient/StereoRenderingManager.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#include "StereoRenderingManager.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
AStereoRenderingManager::AStereoRenderingManager()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void AStereoRenderingManager::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (bAutoEnableOnPlay)
|
||||
{
|
||||
EnableStereo(DefaultStereoMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Log,
|
||||
TEXT("StereoRenderingManager: 立体渲染未自动启动。"
|
||||
"确认视频处理器格式后,在 Details 面板勾选 bAutoEnableOnPlay。"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 公开 API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AStereoRenderingManager::EnableStereo(EStereoOutputMode Mode)
|
||||
{
|
||||
if (Mode == EStereoOutputMode::Off)
|
||||
{
|
||||
DisableStereo();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(供应商回复后检查这里的命令是否与视频处理器匹配):
|
||||
//
|
||||
// UE5 内置立体模式通过以下方式启用。
|
||||
// 如果视频处理器需要特殊格式,可能需要修改 stereo mode 参数。
|
||||
//
|
||||
// 参考命令:
|
||||
// stereo on —— 开启立体渲染(需要 VR 设备或自定义立体设备)
|
||||
// r.StereoRendering 1 —— 软件层立体渲染标志
|
||||
// vr.StereoMode <n> —— 0=SbS full, 1=SbS half, 2=TbB full, 3=TbB half
|
||||
//
|
||||
// 目前使用 r.StereoRendering 的"双视口"方式,兼容性最好。
|
||||
|
||||
switch (Mode)
|
||||
{
|
||||
case EStereoOutputMode::SideBySide:
|
||||
// Side-by-Side:左半=左眼,右半=右眼,总分辨率 2x 宽
|
||||
ExecConsoleCommand(TEXT("r.StereoRendering 1"));
|
||||
ExecConsoleCommand(TEXT("stereo on"));
|
||||
ExecConsoleCommand(TEXT("vr.StereoMode 0")); // TODO: 根据实际效果调整
|
||||
break;
|
||||
|
||||
case EStereoOutputMode::TopBottom:
|
||||
// Top-Bottom:上半=左眼,下半=右眼,总分辨率 2x 高
|
||||
ExecConsoleCommand(TEXT("r.StereoRendering 1"));
|
||||
ExecConsoleCommand(TEXT("stereo on"));
|
||||
ExecConsoleCommand(TEXT("vr.StereoMode 2")); // TODO: 根据实际效果调整
|
||||
break;
|
||||
|
||||
case EStereoOutputMode::FrameSequential:
|
||||
// 本项目屏幕 60Hz,帧序列需要 120Hz,暂不支持
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("StereoRenderingManager: Frame Sequential 需要 120Hz 输出,"
|
||||
"当前屏幕规格不支持,已忽略。"));
|
||||
return;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
// 设置 IPD
|
||||
SetIPD(IPDCm);
|
||||
|
||||
bStereoActive = true;
|
||||
CurrentMode = Mode;
|
||||
UE_LOG(LogTemp, Log, TEXT("StereoRenderingManager: 立体渲染已开启,模式=%s,IPD=%.1fcm"),
|
||||
*ModeToString(Mode), IPDCm);
|
||||
}
|
||||
|
||||
void AStereoRenderingManager::DisableStereo()
|
||||
{
|
||||
ExecConsoleCommand(TEXT("stereo off"));
|
||||
ExecConsoleCommand(TEXT("r.StereoRendering 0"));
|
||||
bStereoActive = false;
|
||||
CurrentMode = EStereoOutputMode::Off;
|
||||
UE_LOG(LogTemp, Log, TEXT("StereoRenderingManager: 立体渲染已关闭,切回 2D"));
|
||||
}
|
||||
|
||||
void AStereoRenderingManager::ToggleStereo()
|
||||
{
|
||||
if (bStereoActive)
|
||||
DisableStereo();
|
||||
else
|
||||
EnableStereo(DefaultStereoMode);
|
||||
}
|
||||
|
||||
void AStereoRenderingManager::SetIPD(float NewIPDCm)
|
||||
{
|
||||
IPDCm = FMath::Clamp(NewIPDCm, 1.f, 10.f);
|
||||
// UE5 以米为单位设置 IPD
|
||||
FString Cmd = FString::Printf(TEXT("vr.HMDIPDInMeters %.4f"), IPDCm / 100.f);
|
||||
ExecConsoleCommand(Cmd);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 内部工具
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AStereoRenderingManager::ExecConsoleCommand(const FString& Cmd)
|
||||
{
|
||||
if (GEngine)
|
||||
{
|
||||
GEngine->Exec(GetWorld(), *Cmd);
|
||||
}
|
||||
}
|
||||
|
||||
FString AStereoRenderingManager::ModeToString(EStereoOutputMode Mode)
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case EStereoOutputMode::SideBySide: return TEXT("SideBySide");
|
||||
case EStereoOutputMode::TopBottom: return TEXT("TopBottom");
|
||||
case EStereoOutputMode::FrameSequential: return TEXT("FrameSequential");
|
||||
default: return TEXT("Off");
|
||||
}
|
||||
}
|
||||
97
ue_client/Source/PlanetClient/StereoRenderingManager.h
Normal file
97
ue_client/Source/PlanetClient/StereoRenderingManager.h
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "StereoRenderingManager.generated.h"
|
||||
|
||||
/**
|
||||
* 立体渲染输出格式
|
||||
*
|
||||
* TODO(等待视频处理器供应商回复):
|
||||
* 确认视频处理器接受哪种格式后,在 DefaultStereoMode 里选择对应值。
|
||||
* - 如果是诺瓦星云/Brompton 处理器,通常接受 SideBySide
|
||||
* - Frame Sequential 需要 120Hz+ 输出,本项目屏幕不需要,基本不选
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EStereoOutputMode : uint8
|
||||
{
|
||||
Off UMETA(DisplayName="关闭(普通 2D 输出)"),
|
||||
SideBySide UMETA(DisplayName="左右并排 Side-by-Side"), // 最常见,优先尝试
|
||||
TopBottom UMETA(DisplayName="上下叠加 Top-Bottom"), // 备选
|
||||
FrameSequential UMETA(DisplayName="帧序列(需 120Hz)"), // 本项目暂不用
|
||||
};
|
||||
|
||||
/**
|
||||
* AStereoRenderingManager
|
||||
*
|
||||
* 放一个在场景里(或在 GameMode 里 SpawnActor),控制立体渲染的开关和参数。
|
||||
* 运行时可以通过 Blueprint 或控制台随时切换模式,方便现场调试。
|
||||
*
|
||||
* TODO(等待供应商回复后填写):
|
||||
* 1. 确认视频处理器输入格式 → 修改 DefaultStereoMode
|
||||
* 2. 确认屏幕分辨率 → 修改渲染分辨率(目前按 5120x1440 SbS 估算)
|
||||
* 3. 调整 IPDCm 到适合大屏幕观看距离的值(推荐 3-6cm,距离越远 IPD 越小)
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class PLANETCLIENT_API AStereoRenderingManager : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AStereoRenderingManager();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 可在 Details 面板调整
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// TODO: 等供应商回复后改为正确格式
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Stereo")
|
||||
EStereoOutputMode DefaultStereoMode = EStereoOutputMode::SideBySide;
|
||||
|
||||
// 瞳距(cm)。大屏幕 + 远距离观看建议 3.0-5.0cm
|
||||
// TODO: 根据实际屏幕尺寸和观看距离校准
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Stereo",
|
||||
meta=(ClampMin="1.0", ClampMax="10.0"))
|
||||
float IPDCm = 6.5f;
|
||||
|
||||
// 是否在 BeginPlay 时自动启用立体渲染
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Stereo")
|
||||
bool bAutoEnableOnPlay = false; // 默认 false,等确认格式后改为 true
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Blueprint 可调用的运行时 API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// 开启立体渲染
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Stereo")
|
||||
void EnableStereo(EStereoOutputMode Mode);
|
||||
|
||||
// 关闭立体渲染,切回普通 2D
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Stereo")
|
||||
void DisableStereo();
|
||||
|
||||
// 切换(当前开 → 关,当前关 → 用 DefaultStereoMode 开)
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Stereo")
|
||||
void ToggleStereo();
|
||||
|
||||
// 调整 IPD(运行时微调景深效果)
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Stereo")
|
||||
void SetIPD(float NewIPDCm);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Planet|Stereo")
|
||||
bool IsStereoActive() const { return bStereoActive; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Planet|Stereo")
|
||||
EStereoOutputMode GetCurrentMode() const { return CurrentMode; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
bool bStereoActive = false;
|
||||
EStereoOutputMode CurrentMode = EStereoOutputMode::Off;
|
||||
|
||||
// 执行实际的控制台命令
|
||||
void ExecConsoleCommand(const FString& Cmd);
|
||||
static FString ModeToString(EStereoOutputMode Mode);
|
||||
};
|
||||
Reference in New Issue
Block a user