Xây dựng Dịch vụ Bản đồ Offline trong UE5: Từ Neo địa lý đến Kết xuất Texture Ảo

1. Tại Sao Tự Xây Dịch vụ Bản đồ Offline trong UE5 Thay vì Dùng API Trực tuyến?

"Xây dựng dịch vụ bản đồ offline trong Unreal Engine" - khi thấy tiêu đề này, nhiều đồng nghiệp phản ứng ngay: UE5 không phải là nền tảng WebGIS, tại sao phải làm bản đồ trong engine? Gọi trực tiếp API tile trực tuyến như Mapbox, CesiumJS hay Gaode/Baidu không tiện hơn sao? Tôi cũng nghĩ như vậy lúc đầu. Cho đến khi làm một dự án hệ thống kiểm tra twin kỹ thuật số cho mỏ khoáng sản vùng xa năm ngoái, khách hàng đưa ra ba yêu cầu cứng: thiết bị hoàn toàn không có mạng công cộng, triển khai đơn lẻ không kết nối internet, tất cả dữ liệu phải được xử lý hoàn toàn nội bộ. Lúc đó chúng tôi thử đóng gói SDK bản đồ trực tuyến vào UE5 Windows standalone package, kết quả khởi động liền báo 403 - không phải vấn đề quyền, mà là engine không thể gửi request ra ngoài: chính sách tường lửa + môi trường sandbox offline + UE5 mặc định vô hiệu hóa HTTP client bên ngoài (đặc biệt với domain không phải HTTPS), ba lớp bảo vệ khiến ngay cả preload cũng thất bại.

Điều này buộc chúng tôi quay lại từ đầu: cái gọi là "dịch vụ bản đồ" cốt lõi là gì? Không phải quả cầu 3D hoành tráng, không phải luồng giao thông thời gian thực, mà là bản nền tham chiếu không gian có thể định vị, truy vấn, chồng lớp và tương tác. Nó cần ba thứ: hỗ trợ hệ tọa độ địa lý (WGS84/UTM), nguồn dữ liệu tile raster hoặc vector, và một pipeline render có khả năng load theo yêu cầu lúc runtime, chuyển đổi projection, chuyển đổi LOD. Niagara, Landscape, Hierarchical LOD System (HLOD) của UE5 và pipeline Nanite+Virtual Texture được tăng cường gần đây, thực ra đã giăng sẵn đường - chỉ là không ai kết nối chúng thành một "mạng bản đồ offline".

Từ khóa "Unreal Engine", "UE5", "dịch vụ bản đồ offline" phía sau, vấn đề thực sự cần giải quyết là một mâu thuẫn kỹ thuật bị đánh giá thấp từ lâu: khả năng render thời gian thực của engine thương mại, và logic tổ chức dữ liệu GIS chuyên nghiệp, có một khoảng cách tự nhiên. Dịch vụ trực tuyến dựa vào server拼接 tile động, projection thời gian thực, lập lịch cache; còn trong scenario offline, những logic này phải đẩy xuống phía client, mà không được kéo chậm frame rate. Cuối cùng chúng tôi từ bỏ cách "mô phỏng dịch vụ trực tuyến", chuyển sang chiến lược hai轨道: "tĩnh hóa dữ liệu địa lý + engine native pipeline驱动". Slice trước dữ liệu bản đồ, chuẩn hóa tọa độ, ảo hóa texture; rồi dùng các cơ chế底层 của UE5 như Streaming Texture, Custom Depth, World Position Offset, để load tile cấp mili giây và ghép nối liền mạch. Toàn bộ quá trình không phụ thuộc plugin thứ ba, implement thuần Blueprint + C++, sau khi đóng gói chỉ tăng 237MB (bao gồm địa hình cơ bản 1:100 toàn cầu + đường giao thông Trung Quốc 15 cấp vector), test trên laptop i7-9750H+RTX2060, 1080p ổn định 60fps, độ trễ zoom dưới 12ms.

Phù hợp với ai? Nếu bạn đang đối mặt với bất kỳ scenario nào sau đây, bài viết này là cho bạn:

  • Làm digital twin công nghiệp, chỉ huy khẩn cấp, mô phỏng quân sự và các dự án có nhu cầu offline mạnh;
  • Đã có dữ liệu địa lý định dạng GeoJSON/TIFF/MBTiles sẵn, nhưng không biết cách căn chỉnh tọa độ thực chính xác trong UE5;
  • Bị kẹt với những câu nói như "UE5 không hỗ trợ WGS84", "không thể làm projection Mercator", không dám đụng đến visualization không gian;
  • Hoặc bạn chỉ tò mò: khi loại bỏ mọi ảo tưởng về cloud service, một thế giới địa lý 3D thuần túy, gắn liền với đĩa cục bộ, rốt cuộc được xây dựng từ đầu như thế nào?

Dưới đây tôi sẽ trình bày tất cả những坑 đã dẫm, giải pháp đã verify, thông số đã benchmark trong một năm qua. Không nói suông, chỉ thao tác có thể click trong UE5 editor, code có thể compile trong C++, kết quả có thể chạy sau khi đóng gói.

2. Bản chất của Bản đồ Offline: Không phải "Tile Loader", mà là "Hệ thống Neo Tọa độ Không gian Địa lý"

Nhiều người nghe "bản đồ offline" phản ứng đầu tiên là viết HTTP client đọc file MBTiles cục bộ. Hướng đi này không sai, nhưng trong UE5 sẽ ngay lập tức đụng ba bức tường: tường thread-safe, tường memory, tường tọa độ. UE5 GameThread cấm IO blocking, mà MBTiles là SQLite database, một query đơn lẻ có thể block vài chục mili giây; texture RGBA8 sau giải mã tile mỗi tấm 2MB, load 100 tấm đồng thời sẽ explode memory; chíết lạc nhất là - trong spec MBTiles tất cả tile đều dựa trên Web Mercator (EPSG:3857), còn UE5 đơn vị world mặc định là centimet, trục Z hướng lên, không có khái niệm kinh vĩ độ. Bạn dán một tile (12, 3456, 7) lên Landscape, nó sẽ xuất hiện ở giữa Thái Bình Dương, hoặc chui xuống lòng đất.

Vì vậy bước đầu tiên, phải重构 nhận thức: "dịch vụ bản đồ offline" trong ngữ cảnh UE5, nhiệm vụ cốt lõi không phải "hiển thị ảnh", mà là thiết lập quan hệ ánh xạ hai chiều không mất dữ liệu từ kinh vĩ độ (Lon/Lat) đến tọa độ world UE5 (X/Y/Z). Một khi quan hệ này được neo, mọi thứ sau đó như load tile, đặt model, lập kế hoạch đường đi mới có ý nghĩa. Chúng tôi cuối cùng dùng "chuỗi chuyển đổi tọa độ ba cấp" để thực hiện:

2.1 Cấp một: Tọa độ địa lý → Tọa độ mặt phẳng chiếu (WGS84 → Web Mercator)

Đây là quy trình tiêu chuẩn trong lĩnh vực GIS, nhưng UE5 không có sẵn hàm. Chúng tôi dùng C++ đóng gói thư viện PROJ thu gọn (chỉ giữ lại hàm cốt lõi proj_trans), compile thành static library link vào module UE5. Code quan trọng như sau:

// GeoCoordHelper.h
#pragma once
#include "CoreMinimal.h"
#include "Math/Vector2D.h"
#include "Math/Vector.h"

USTRUCT(BlueprintType)
struct FGeographicCoord
{
    GENERATED_BODY()
    UPROPERTY(BlueprintReadOnly) float Vĩ độ;    // WGS84, độ
    UPROPERTY(BlueprintReadOnly) float Kinh độ; // WGS84, độ
};

// GeoCoordHelper.cpp
#include "GeoCoordHelper.h"
#include "proj.h"
#include "proj_experimental.h"

FVector2D UGeoCoordHelper::ChuyểnWGS84SangMercator(const FGeographicCoord& Tọa độVào)
{
    PJ_CONTEXT* Bối cảnh = proj_context_create();
    PJ* Trình chuyển = proj_create_crs_to_crs(Bối cảnh, "EPSG:4326", "EPSG:3857", nullptr);
    
    PJ_COORD tọa độ;
    tọa độ.lpzt.lam = FMath::DegreesToRadians(Tọa độVào.Kinh độ); // rad
    tọa độ.lpzt.phi = FMath::DegreesToRadians(Tọa độVào.Vĩ độ);  // rad
    tọa độ.lpzt.z = 0.0;
    tọa độ.lpzt.t = HUGE_VAL;

    PJ_COORD kết quả = proj_trans(Trình chuyển, PJ_FWD, tọa độ);
    
    FVector2D Ra;
    Ra.X = kết quả.xy.x; // mét easting
    Ra.Y = kết quả.xy.y; // mét northing
    
    proj_destroy(Trình chuyển);
    proj_context_destroy(Bối cảnh);
    return Ra;
}

Lưu ý: Không dùng "công thức đơn giản hóa" lan truyền trên mạng (như x = lon * cos(lat)), đó chỉ là xấp xỉ, sai số vượt 3km khi vượt quá 10° vĩ độ. Chúng tôi đã test thực tế, ở vùng vĩ độ 45° bắc, công thức đơn giản khiến model ống khói nhà máy điện lệch 800 mét - đây là thảm họa cho hệ thống kiểm tra.

2.2 Cấp hai: Tọa độ mặt phẳng chiếu → Tọa độ cục bộ UE5 (Web Mercator → Local Space)

Đây mới là khó khăn đặc thù của UE5. Tọa độ Web Mercator đơn vị là "mét", nhưng điểm gốc của nó nằm ở giao điểm xích đạo và kinh tuyến gốc (0,0), còn UE5 world origin (0,0,0) có thể đặt tùy ý. Chúng tôi phải chọn một điểm neo địa lý (Geographic Anchor), làm (0,0,0) của UE5 world. Ví dụ, tọa độ GPS của một trạm biến áp (116.3975°Đ, 39.9087°B), chúng tôi đặt nó làm origin UE5. Thì bất kỳ tọa độ kinh vĩ độ nào, trước chuyển sang Web Mercator, rồi trừ đi tọa độ Web Mercator của điểm neo, sẽ ra giá trị X/Y trong UE5:

// Giả sử neo: Quảng trường Thiên An Môn Bắc Kinh (116.3975, 39.9087)
FVector2D NeoMercator = ChuyểnWGS84SangMercator(FGeographicCoord{39.9087f, 116.3975f});
FVector2D Mục tiêuMercator = ChuyểnWGS84SangMercator(Tọa độMục tiêu);
FVector2D XY_Địa phương = Mục tiêuMercator - NeoMercator;
// Giá trị Z do dữ liệu độ cao quyết định, xem phần sau
FVector Vị tríUE5 = FVector(XY_Địa phương.X, XY_Địa phương.Y, ĐộCaoTínhBằngCM);

Chú ý: Hướng trục Y ở đây! Web Merca

Đăng vào ngày 10 tháng 7 lúc 08:44