暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

虚幻引擎加载Ganos数据实战

原创 Ganos全空间数据库 2024-02-21
570

1. 简介

1.1 关于Ganos

Ganos是阿里云数据库产品事业部研发的新一代云原生位置智能引擎,它将时空数据处理能力融入了云原生关系型数据库PolarDB、云原生多模数据库Lindorm、云原生数据仓库AnalyticDB和云数据库RDS PG等核心产品中。Ganos目前拥有几何、栅格、轨迹、表面网格、体网格、3D实景、点云、路径、地理网格、快显十大核心引擎,为数据库构建了面向新型多模多态时空数据的存储、查询、分析、服务等一体化能力。



本文介绍的相关能力,依托阿里云云原生关系型数据库PolarDB建设输出。

1.2 关于UE

虚幻引擎(Unreal Engine)是一款由Epic Games开发的游戏引擎,它被广泛应用于游戏开发、虚拟现实和增强现实等领域。如果想用虚幻引擎加载地理数据,可以借助 Cesium 开发的 Cesium for Unreal 插件。

Cesium for Unreal 是一个免费的开源插件,它可以在虚幻引擎中使用Cesium的3D地球和地理空间数据。这使得开发者能够在虚幻引擎中创建和展示高质量、准确的地理空间视觉化,例如地形、三维建筑、道路和定位数据等。它为游戏设计师和其他虚幻引擎用户提供了一个功能强大的工具来创建真实世界的环境和场景。

Ganos 可以配合 Cesium for Unreal 插件将数据库中的数据展现在虚幻引擎中。

2. 加载遥感影像数据

2.1 数据概况

测试数据为某区域的四幅Landsat 8遥感影像,形如:

2.2 数据入库

  • 创建测试表 test_raster:

CREATE TABLE public.test_raster (
	id int4 NOT NULL,
	rast raster NULL
);
  • 将四幅测试数据从OSS导入(仅展示第一条入库语句):

INSERT INTO test_raster VALUES(1, ST_ImportFrom('chunk_table','OSS://YOUR_OSS_AK:YOUR_OSS_SK@OSS_REGION/YOUR_BUCKET/YOUR_TIF_FILE'));
...

ST_ImportFrom 函数的详细教程可参考这里

  • 将四幅影像进行镶嵌,并创建镶嵌后数据,并设置其 ID 为5:

INSERT INTO test_raster VALUES(5, ST_MosaicFrom(Array(select rast from test_raster), 'chunk_table','','{"color_balance":true,"nodata":true,"nodatavalue":0}'));

开启匀色(color_balance),使镶嵌后的影像整体效果更加一致。

开启支持 nodata 并设置 nodatavalue,可以使匀色时接边部分更好的衔接在一起。

  • 更新镶嵌后数据集:

UPDATE test_raster SET rast = ST_BuildPyramid(ST_ComputeStatistics(rast)) WHERE ID = 5;

计算统计信息(ST_ComputeStatistics)可以在取瓦片时开启拉伸,显示效果更好。

创建金字塔(ST_BuildPyramid)可以加快瓦片的读取。

2.3 服务编写

Cesium for Unreal 当前版本(2.2.0)支持TMS协议的地图数据,因此需要将数据库内镶嵌后的影像发布为TMS服务。为便于展示,我们选用Node.js(建议版本18及以上)编写服务,用户也可自行选用合适的编程语言编写。

  • 创建工作目录,命名为 tms 并进入该目录:
mkdir tms;cd tms 
  • 在目录下安装依赖包:
npm i express pg xml2js -S
  • 在目录下创建 tms.js 文件,内容为:
const express = require('express');
const { Pool } = require('pg');
const xml2js = require('xml2js');

// 填写数据库的连接参数
const CONNECTION = { user: 'YOUR_USER', password: 'YOUR_PWD', host: 'YOUR_DB_HOST', database: 'YOUR_DB', port: YOUR_DB_PORT }
// 填写表信息
const TABLE_NAME = "test_raster"
// 填写字段信息
const RASTER_COLUMN = "rast"
const RASTER_ID = "5"
// 指定最大Z值,可根据数据实际情况调节
const MAX_Z = 14

// 创建连接池
const pool = new Pool(CONNECTION)

express()
    // 动态构建tilemapresource.xml文件
    .get('/tilemapresource.xml', async (__, res) => {
        const sql = `
        SELECT ST_NAME(${RASTER_COLUMN}) AS NAME, BOX FROM 
        (SELECT ST_TRANSFORM(ST_SETSRID(ST_ENVELOPE(${RASTER_COLUMN}), ST_SRID(${RASTER_COLUMN})), 4326)::BOX AS BOX,
        ${RASTER_COLUMN} FROM ${TABLE_NAME} WHERE ID = ${RASTER_ID}) SUB;`
        const { rows: [{ name: Title, box }] } = await pool.query(sql)
        const [_, maxx, maxy, minx, miny] = /\((.*?),(.*?)\),\((.*?),(.*?)\)/.exec(box)
        const tilemapresource = new xml2js.Builder({ explicitArray: true })
            .buildObject({
                TileMap: {
                    $: { "version": "1.0.0", "tilemapservice": "http://tms.osgeo.org/1.0.0" },
                    Title,
                    SRS: 'EPSG:3857',
                    BoundingBox: { $: { minx, miny, maxx, maxy } },
                    Origin: { $: { minx, miny } },
                    TileFormat: { $: { width: '256', height: '256', 'mime-type': "image/png", extension: "png" } },
                    TileSets: {
                        $: { profile: 'mercator' },
                        TileSet: Array.from({ length: MAX_Z + 1 }, (x, z) => ({ $: { href: `./tile/${z}`, order: z } }))
                    }
                }
            })
        res.set("Content-Type", "text/xml").send(tilemapresource)
    })
    .get('/tile/:z/:x/:y\.:png', async ({ params: { z, x, y } }, res) => {
        [z, x, y] = [z, x, y].map(x => parseInt(x))
        // 调整为TMS标准Y
        y = (Math.pow(2, z) - 1) - y
        // 指定拉伸方式为按正态分布拉伸
        // 可按需求效果调整展示的波段,本例中调整为0,2,1波段
        const sql = `
        SELECT (ST_ASTILE(${RASTER_COLUMN},ST_TRANSFORM(ST_TILEENVELOPE(${z},${x},${y}),
        ST_SRID(${RASTER_COLUMN})),'{"strength":"ratio","bands":"0,2,1","alpha":true}')).data tile 
        FROM ${TABLE_NAME} WHERE ID = ${RASTER_ID};`
        const { rows: [{ tile }] } = await pool.query(sql)
        res.set("Content-Type", "image/png").send(tile)
    })
    // 指定端口为5500,可自行修改为合适的端口
    .listen(5500, () => console.log('tms server started.'))
  • 在目录下运行服务:
node ./tms.js

此时访问地址 http://localhost:5500/tilemapresource.xml,将返回TMS服务的元信息。针对本例数据,形如:

<TileMap version="1.0.0" tilemapservice="http://tms.osgeo.org/1.0.0"> 
  <Title>LC08.TIF</Title>  
  <SRS>EPSG:3857</SRS>  
  <BoundingBox minx="127.47608063333244" miny="43.51624241211837" maxx="132.59324217543937" maxy="47.09722015288969"/>  
  <Origin minx="127.47608063333244" miny="43.51624241211837"/>  
  <TileFormat width="256" height="256" mime-type="image/png" extension="png"/>  
  <TileSets profile="mercator"> 
    <TileSet href="./tile/0" order="0"/>  
    <TileSet href="./tile/1" order="1"/>  
    <TileSet href="./tile/2" order="2"/>  
    <TileSet href="./tile/3" order="3"/>  
    <TileSet href="./tile/4" order="4"/>  
    <TileSet href="./tile/5" order="5"/>  
    <TileSet href="./tile/6" order="6"/>  
    <TileSet href="./tile/7" order="7"/>  
    <TileSet href="./tile/8" order="8"/>  
    <TileSet href="./tile/9" order="9"/>  
    <TileSet href="./tile/10" order="10"/>  
    <TileSet href="./tile/11" order="11"/>  
    <TileSet href="./tile/12" order="12"/>  
    <TileSet href="./tile/13" order="13"/>  
    <TileSet href="./tile/14" order="14"/> 
  </TileSets> 
</TileMap>

2.4 创建场景

  • 在虚幻引擎中安装 Cesium for Unreal 插件,可参考这里
  • 创建一个空白的游戏蓝图项目:

  • 在菜单栏中点击 文件-新建关卡-空白关卡,创建一个新关卡。
  • 在菜单栏中点击 编辑-插件-已安装-Geospatial,勾选 Cesium for Unreal,并重启项目。

  • 可参照这里调整默认的启用边界检查(World Bounds Checks)和自动曝光(Auto Exposure)选项,使整个场景效果正常,并连接到自己的 Cesium Ion 账号,以便正常使用底图服务和地形服务。为方便调试,场景可设置为无光照,实时渲染。
  • 在 Cesium for Unreal 插件中点击 Cesium SunSky 和 Cesium World Terrain + Bing Maps Aerial 后的“+”按钮,将默认光照和底图添加到场景中:

2.5 加载服务

  • 修改大纲视图中 CesiumGeoreference0 的经纬度位置为该影像的中心点大概位置:

CesiumGeoreference0 为自动创建的默认 CesiumGeoreference。

影像中心点大概位置可根据通过SQL语句查看:

SELECT ST_Centroid(ST_Transform(ST_Envelope(rast), 4326)) FROM test_raster WHERE ID = 5;
st_centroid                                |
-------------------------------------------|
POINT (130.0346614043859 45.30673128250403)|

  • 在 Cesium World Terrain 下添加 Cesium Tile Map Service Raster Overlay 组件:

  • 设置 Url 为 http://localhost:5500/tilemapresource.xml,并设置 Material layer Key 为 Overlay1:

  • 此时可以在场景中看见叠加的图层:

3. 加载倾斜摄影数据

3.1 数据概况

测试数据为某体育场的倾斜摄影数据,形如:

3.2 数据入库

  • 将测试数据从OSS中导入:

SELECT ST_ImportOSGB('test_osgb','OSS://YOUR_OSS_AK:YOUR_OSS_SK@OSS_REGION/YOUR_BUCKET/YOUR_OSGB_PATH','{"parallel":16,"project":"olympic"}');

设置并行度(parallel)为16可加快入库速度。

设置项目名(project)为 olympic 可以方便用户在表中区分不同项目的数据。

  • 执行成功后将创建三张表:

test_osgb:元数据表。

test_osgb_tile:瓦片表。

test_osgb_tileset:tileset.json表。

3.3 服务编写

为便于展示,我们选用Node.js(建议版本18及以上)编写服务,将入库的倾斜数据以 3dtile 协议发布。用户也可自行选用合适的编程语言编写。

  • 创建工作目录,命名为 3dtile 并进入该目录:
mkdir 3dtile;cd 3dtile
  • 在目录下安装依赖包:
npm i express pg -S
  • 创建 3dtile.js 文件,内容为:
const express = require('express');
const { Pool } = require('pg');

// 填写数据库的连接参数
const CONNECTION = { user: 'YOUR_USER', password: 'YOUR_PWD', host: 'YOUR_DB_HOST', database: 'YOUR_DB', port: YOUR_DB_PORT }
// 填写表前缀
const TABLE_PREFIX = "test_osgb"
// 填写项目名
const PROJECT_NAME = "olympic"

// 创建连接池
const pool = new Pool(CONNECTION)

express()
    .get('/tileset.json', async (_, res) => {
        const sql = `
        SELECT PROJECT_ID FROM ${TABLE_PREFIX} WHERE PROJECT_NAME = '${PROJECT_NAME}';`
        const { rows: [{ project_id }] } = await pool.query(sql)
        res.redirect(`/tileset/${project_id}/${project_id}`)
    })
    .get('/tileset/:project_id/:tileset_id', async ({ params: { project_id, tileset_id } }, res) => {
        const sql = `
        SELECT TILESET FROM ${TABLE_PREFIX}_TILESET WHERE PROJECT_ID = '${project_id}' and UID='${tileset_id}';`
        const { rows: [{ tileset }] } = await pool.query(sql)
        res.set("Content-Type", "text/json").send(tileset)
    })
    .get('/b3dm/:project_id/:b3dm_id', async ({ params: { project_id, b3dm_id } }, res) => {
        const sql = `
        SELECT ST_ASB3DM(tile) TILE FROM ${TABLE_PREFIX}_TILE WHERE PROJECT_ID = '${project_id}' and UID='${b3dm_id}';`
        const { rows: [{ tile }] } = await pool.query(sql)
        res.set("Content-Type", "application/octet-stream").send(tile)
    })
    // 指定端口为5500,可自行修改为合适的端口
    .listen(5500, () => console.log('3dtile server started.'))
  • 在目录下运行服务:
node ./3dtile.js

此时,访问地址 http://localhost:5500/tileset.json,将返回根节点的 tileset.json。

3.4 加载服务

  • 参照2.4节,创建新的 Cesium 场景。
  • 在菜单栏中点击 窗口-放置Actor,在搜索框中搜索 Cesium Georeference:

  • 将其拖放到场景中,在大纲中将其重新命名为 CesiumGeoreferenceLocal ,并调整其 Orgin Placement 为 True origin :

当前版本的 Ganos 生成的 3DTiles 数据使用的是相对坐标系而非 ECEF(Earth-centered, Earth-fixed)坐标系,因此需要将其手动绑定到地球上某个特定位置。

  • 在 Cesium for Unreal 插件中点击 Blank 3D Tiles Tileset 后的“+”按钮,添加一个空的 3DTileset 实例到场景中,并在大纲中拖拽新创建的 Cesium3DTileset 使之位于根节点下:

  • 在 Cesium3DTileset 组件下添加一个 Cesium Globe Anchor 组件:

  • 若该倾斜数据在其 metadata.xml 中存储了原点信息,则入库后可以通过SQL语句在其元数据表中查询原点:
SELECT ST_Transform(anchor, 4326) FROM test_osgb WHERE project_name = 'olympic';
st_transform                                       |
---------------------------------------------------|
POINT(111.22596412000003 30.69554377700044 313.281)|
  • 将该原点信息赋予创建的 Cesium Globe Anchor 组件,并设置 Georeference 为 CesiumGeoreference0:

  • 设置 Cesium3DTileset 的 Georeference 为 CesiumGeoreferenceLocal,Source 为 FromUrl,Url 为 http://localhost:5500/tileset.json:

  • 选中 Cesium3DTileset(实例),按“F”键使视角聚焦到模型位置。在大纲中选中 CesiumGeoreference0,点击  Place Georeference Origin Here 按钮以固定视角:

  •  调整视角,若发现数据与底图存在偏移,可以点击 Cesium Globe Anchor 组件的 Snap To East South Up 按钮:

  • 此时可以在场景中看见叠加到底图上的模型:

4. 其他引擎兼容

以上教程可以通过 Cesium for Unity 包推广到 Unity 引擎或 Cesium for Omniverse 扩展推广到 Nvidia Omniverse 平台。详情可参考 Cesium for Unity 教程及 Cesium for Omniverse 教程

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论