Compare commits
5 Commits
v0.43.0
...
feature/ue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c97dd83f3e | ||
|
|
a4e6ce7489 | ||
|
|
7ffc8537e4 | ||
|
|
4dd396ea65 | ||
|
|
1e6f4b338b |
@@ -15,6 +15,7 @@ from app.api.v1 import (
|
|||||||
bgp,
|
bgp,
|
||||||
system_control,
|
system_control,
|
||||||
tv,
|
tv,
|
||||||
|
ue_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
api_router = APIRouter()
|
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(visualization.router, prefix="/visualization", tags=["visualization"])
|
||||||
api_router.include_router(bgp.router, prefix="/bgp", tags=["bgp"])
|
api_router.include_router(bgp.router, prefix="/bgp", tags=["bgp"])
|
||||||
api_router.include_router(tv.router, prefix="/tv", tags=["tv"])
|
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