跳转到内容

地形

地面是大多数 3D 场景里画得最大的东西,也是网格最不擅长的东西:相机近处三角形太多,而真正需要的地方又太少。Terrain 组件把它画成一棵围绕视点的四叉树,每个方块都是同一块小面片,所以脚下的山丘用细格子画,地平线上的同一座山丘用大好多倍的格子画。

给某个实体加一个 Terrain 组件。地形放在该实体的位置;它的子实体就是四叉树的节点,在场景运行时(以及编辑器的场景视图里)生成,随视点移动而调整。没有任何东西被烘焙进场景,除了组件本身也没有任何东西被保存。

地面有两种,由 extent 选择:

  • Area(区域):以实体为中心、sizeX 乘 sizeZ 的一片地面,来自高度图;没有高度图时,是在区域所在的世界位置上读取其生成器地面的一扇窗口(所以尺寸和噪声相同的两块区域并排时无缝相接,区域与其生成器的无限地面是同一片地面;移动区域移动的是窗口,不是地面)。可以被雨水侵蚀,也可以用 splatmap 上色。
  • Infinite(无限):由一个生成器(provider,见下)在世界中任意一点读取的地面,所以没有边缘。同一个生成器、世界、种子和版本在任何地方都是同一片地面,编辑器里、游戏里、服务器上都一样。它围绕视点绘制到 drawDistance 为止。
字段 作用
extent Area 或 Infinite,见上。
heightmap 仅区域。按数据读取的图:取文件里原样的字节,不做色彩转换,所以它以原本的 PNG 发布(打包时被压缩的高度图会让地形失败并说明原因)。为空则用噪声生成地面。
heightmapFormat 高度图如何存放高度。灰度:灰度(或红色)通道,按文件的位深读取:16 位 PNG 读 16 位(World Machine、Gaea 等大多数工具的导出格式),8 位图只有 256 级。红 + 绿:8 位图里红色(高字节)和绿色(低字节)拼成的 16 位。
sizeX、sizeZ、height 区域的世界尺寸,以及最高处地面的世界高度。对无限地面,sizeX 是 frequency 座山丘铺开的跨度,sizeZ 不使用。
cellSize 地面网格最细的格子,以世界单位计:视点近处地面按它绘制。再远一些,格子逐级加倍。
detail 最细格子在地面变粗之前能延伸多远的倍率。大于 1 时远处更细,三角形也更多。
drawDistance 仅无限地面。地面从视点向外绘制多远。
collision、collisionDistance 3D 刚体和角色是否与地面碰撞,以及在每个运动物体周围多远的范围内为碰撞构建地面。collisionLayer 和 friction(高级)是地面的碰撞层和摩擦。
provider、world、generatorVersion 无限地形,或没有高度图的区域。生成地面的生成器(estella:noise 为引擎自带噪声,或游戏注册的生成器)、游戏给这个世界起的名字,以及这个世界生成时用的生成器版本。
seed、frequency、octaves、persistence、ridged 噪声:换一个种子就是另一片地面;frequency 是横跨有几座山丘;ridged 从起伏的丘陵到尖锐的山脊。
resolution 仅区域。区域噪声地面每边的采样数。
warp 0 到 1:噪声被它自己的另一层噪声扭曲多远,得到蜿蜒的山谷和弯折的山脊,而不是圆圆的山丘。
erosion 0 到 4:雨水把地面侵蚀多少(见下)。0 保持生成时的样子。
splatmap 仅区域。一张图,四个通道是第 1 到 4 层的权重。为空则按高度和坡度上色。
splatmap2 仅区域。第 5 到 8 层的权重,与第一张一起读取。
layers 绘制地面所用的地形层,最多八个,按地表图权重的顺序。为空则用四种图层颜色或 Terrain 材质的贴图。
rockSlope、dirtHeight、snowHeight 地面要多陡才变成岩石,以及草地转为泥土、雪开始出现的高度(占 height 的比例)。
layerColor0–3 草、泥土、雪、岩石:没有材质时地面绘制用的颜色。
material 用 Terrain 着色器的材质(见下);地面有地形层时用 Terrain Layers 着色器。为空则用内置材质。

改 detail、drawDistance 或图层颜色在下一帧生效。改其他任何字段都会重新构建地面。

在区域上,erosion 让雨水一滴一滴落在场上,为 1 时场上每个采样点落一滴:每滴向下坡流,在还能多带泥土的地方冲刷地面,在流速变慢的地方把泥土放下,于是坡上留下冲沟,冲沟之间的山脊更锋利,谷底积起淤泥。同一个种子侵蚀出同一片地面。它对高度图的场和噪声的场都有效。水流过的地方草被带走,换成泥土,所以水道能看得出来。在大场上下雨要花好几秒,所以它被分摊到多帧里进行,并在进行中显示进度(见分帧构建)。

无限地面没有让雨水流走的边缘,所以它的侵蚀是噪声自己的:噪声的每个八度在它下面的八度已经很陡的地方被压低。这样会在尖锐的山脊之间留下平缓的山谷,和雨水的效果很像,除了噪声本身之外不花额外代价。它不画泥土水道。

地形与水面示例、流向大海的河流和前景的海面

地形与水面示例:分层的地面、河流和海面,全部来自 Terrain 和 Water 组件。

地形层(.esterrainlayer 文件;编辑器里 新建 → 新建地形层)是一种地面——草、土路、沙、岩石——由美术为它制作的贴图组成。Terrain 列出它所用的地形层(layers,最多八个),地表图给出权重:第一张的红、绿、蓝、透明通道是第 1 到 4 层,第二张是第 5 到 8 层。没有地表图时,前四层按高度和坡度作为草、土、雪、岩上色;生成世界的群落可以为全部八层上色。

字段 含义
albedo 颜色贴图,sRGB。为空则为白色,由色调着色。
normal 切线空间法线贴图,绿色朝上(OpenGL 约定)。为空则平坦。
mask 红为环境遮蔽,绿为粗糙度,蓝为高度。为空:无遮蔽,用 roughness,高度取颜色亮度。
tile 贴图重复一次覆盖的世界单位。
tint 与颜色相乘。
roughness 没有遮罩贴图时的粗糙度。
normalStrength 法线贴图的强度:1 为原样,0 为平坦。
heightBlend 可选。这一层自己的高度在与别的层相交处起多大作用:0 只按涂色混合(草到沙),1 按各层高度混合(石头从草里露出)。多层相遇时,每层按它在此处所占的比例计入。缺省则用材质的 u_heightBlend。

地面由 Terrain Layers 着色器(builtin:terrain-layers)绘制。每一层按世界位置铺设,近处去除重复,远处混入更大的平铺;层与层交界处遮罩高度更高的一层显露(草间露出石头、石缝里填着沙);坡面上同时从侧面投影,悬崖不会被拉伸。法线贴图和遮罩的遮蔽、粗糙度参与光照。材质控制效果:u_sharpness、u_heightBlend、u_blendNoise、u_antiTile、u_farScale、u_macroVariation、u_triplanarFrom(多陡开始读侧面)和 u_wetLevel/u_wetBand,含义同下文 Terrain 着色器。

美术每层交付:三张贴图,正方形,边长为 2 的幂(1024 足够;打包时取最大一层的尺寸,最大 1024,小游戏上 512),四方连续,法线和遮罩按数据保存(不做色彩管理)。地形构建时各层贴图会打包成纹理数组,地形层文件或其贴图保存后会重新打包。

Terrain 的 Details,以及地面的形状和高度来源

Terrain 组件:先是图层,然后是地面的形状和高度的来源。

四个图层是草、泥土、雪和岩石。地面每一点在每个图层上都有一个权重,来自 splatmap 或来自那里有多高多陡,Terrain 着色器(builtin:terrain)把它们画出来。纹理按世界位置平铺,所以不管地面怎么切分,图层都连续不断;岩石从三个方向投影,所以悬崖不会被拉成一团。

参数 作用
u_layer0–3 各图层的纹理:草、泥土、雪、岩石。
u_tint0–3 各图层的颜色,与它的纹理相乘。
u_tile0–3 每个图层一次重复覆盖多少世界单位。
u_sharpness 接缝处权重更大的图层接管多少:1 是普通混合,更高则边缘更硬。
u_heightBlend 接缝跟随图层自身起伏(即它们的亮度)的程度:草地退去之前,岩石先从草的缝隙里露出来;沙子填满石头之间的低处。0 是按权重普通混合。
u_antiTile 每个图层再读一次的比例,这次旋转过、平铺得更宽,并用缓慢的噪声混入,让整片地上看不出重复。它随距离淡出,远处的重复小到看不见。
u_macroVariation 叠在每个图层上的大尺度、缓慢的亮度变化,让大片草地不是同一种颜色。
u_relief 图层的起伏(亮度当作高度)在光照里画多深,以世界单位计。0 让地面保持平滑。
u_wetLevel、u_wetBand 水边的地面:低于 u_wetLevel + u_wetBand(世界高度)时变暗变亮泽,在 u_wetLevel 处最强。带宽为 0 则不画。
u_roughness 干燥处地面的粗糙度。

着色器的其他参数(u_heightPages、u_lodRange0 之类)由 Terrain 组件每帧写入;材质不用管它们。

地面由一棵四叉树切分。树上每个节点,在每一级,都用同一块 32 乘 32 格的面片绘制,缩放到节点的方块大小:第 0 级节点宽 32 * cellSize,每往上一级宽一倍,所以格子也大一倍。每一级有一个离视点的范围。第 0 级的范围是 2.5 * 32 * cellSize * detail,每一级的范围是下一级的两倍。离视点比下一级的范围还近的节点会被分成四个子节点,所以地面在视点周围最细,随距离逐级变粗。区域有足够的级数让顶层节点和区域一样宽;无限地面有足够的级数让顶层的范围达到 drawDistance。

节点是地形实体下普通的 MeshRenderer 子实体,全部用同一个网格和同一个材质,渲染器把它们当作实例绘制:整片地面只是寥寥几次绘制。面片本身是平的。Terrain 着色器从节点的页(见下)读出地面高度,把每个顶点抬上去,并从同一页取出地面的法线。

在一级范围的末段,着色器把每个顶点滑到上一级的网格上(几何形变,geomorphing)。到范围边缘时,细节点的每个顶点都落在旁边粗节点有顶点的位置,所以两级沿同一条线相接:没有裂缝,也就不需要任何东西去遮,视点移动时也不会有东西突然跳变。

节点绘制所用的高度存在一个页里:节点的方块每边采样 64 次(每个面片格子两个采样),边缘外再多一个,这样边缘处的坡度也已知。页把高度按地面的高度范围以 16 位存储,连同法线放在一张图集纹理里,四个图层的权重放在另一张里;每张图集 2048 乘 2048,能放 900 页。粗节点的页用同样的 64 个采样覆盖更大的地面,所以它的细节随格子一起变粗。

页按四叉树的需要生成,每帧在时间预算内做几页,粗的、近的优先。节点的页做好之前,用它最近的、已有页的祖先节点代为绘制,所以地面立刻就在,随着页陆续做好而变清晰。图集满了时,最久没被绘制的页让出位置。

生成页和用雨水侵蚀区域都分摊到多帧里进行,而不是一次做完,所以都不会卡住某一帧。进行期间,它们和进度一起(“Building the terrain”、“Eroding the terrain”)列在编辑器的状态栏里,游戏也可以读取同一份列表来显示加载画面。

地形是可选模块:场景里有 Terrain,或脚本导入了 esengine/terrain(如下),包里才会带上它;两者都没有的游戏不带任何地形代码。

可以问地面某处有多高,用来把东西放上去:

import { TerrainsRes } from 'esengine/terrain';
const terrains = app.getResource(TerrainsRes);
const y = terrains.heightAt(x, z); // 世界高度,没有地形的地方为 null
const n = terrains.normalAt(x, z); // 那里的单位法线,或 null

它们读的是地表:cellSize 的格点网,每个节点取数据源在那里的高度,每格沿 (1, 0) 角到 (0, 1) 角的对角线切成两片三角形。这正是视点近处最细一级画出来的三角面,也是物体碰撞的三角面,所以查询、射线和画出的三角面一致(差别只在 GPU 的 16 位高度编码,几百单位高的地形是几毫米),而且答案不随摄像机变化。normalAt 是那里三角面的法线。任何一点都能回答,不管那里的页做没做好。区域的图读完、侵蚀做完之前,heightAt 返回 null,terrains.loading 为 true。稍后才应答的生成器,在某点周围的样本到达之前,该点返回 null。

terrains.status(entity) 说明地形所处的状态:loading、ready,或带 reason 的 failed。高度图读不到、生成器没注册、或生成器已没有所要的版本时,地形失败:什么都不画,查询返回 null,而不是在游戏期待自己地面的地方长出噪声。

打开 collision 后,地面以 32 格为一块,在每个运动的 3D 刚体和每个角色周围 collisionDistance 内为 3D 物理世界构建:它脚下那块立刻构建,其余每帧几块、由近及远,没有东西靠近后释放。每块是属于地形实体的静态网格刚体,所以射线或接触打到地面时报告的是地形实体。地形重建期间旧块保留到新块替换它们,站在上面的东西不会掉下去。terrains.collisionReadyAt(x, z) 说明某点下的地面是否已构建好,游戏可以等它再往那里放东西(稍后才应答的生成器在样本到达后才构建那块)。离任何运动物体都远的地方,物理世界里没有可碰的东西;terrains.raycast(origin, direction, maxDistance) 不经物理、在任何地方都与同一组三角面求交,可用于远距离视线、拾取或寻找落点。

碰撞块用网格而不用压缩的高度场形状是有意的:高度场形状会量化样本、按自己的方式切分格子,碰撞的地面就不再是画出来、查询到的那片地面。

这些块经由 CollisionGeometry 资源(esengine)交出,任何在游戏运行中生成静态几何的系统都可以用它;3D 物理世界为其中每一片保留一个静态刚体。

游戏在需要的地方修改地面(压平一块地基、一个停机坪、一段路基)用的是修改器:以世界位置和该处地面高度为输入的函数,各自作用于世界中的一块方形范围。它们按设置的先后叠加在地形的地面上,所有读取方都跟随它们:画出来的地面、heightAt 和 normalAt、射线和碰撞。只有修改器触及的页和碰撞块会重做,立在地面上的东西(植被、水、生成世界)会重新放置。

import { flattenModifier } from 'esengine/terrain';
const terrains = app.getResource(TerrainsRes);
const level = terrains.heightBeneathAt(x, z)!; // 地面生成时的高度,不含任何修改器
terrains.setModifier(flattenModifier('plot 1', { type: 'rect', x, z, halfX: 40, halfZ: 30, yaw }, level, 12));
terrains.deleteModifier('plot 1'); // 地面恢复原样

带 flatten 的 WorldZone 就是这样压平它的地面的。

findPath 在地面上搜索从一个地方到另一个地方、受坡度限制的步行者能走的路:游戏在信任一个生成的世界之前,用它检查出生点、任务目标或村庄是否可达。它在搜索经过的地方读取地面(包括修改器),不需要预先烘焙,无限地形上也能用。

const terrains = app.getResource(TerrainsRes);
const waters = app.getResource(WatersRes);
const way = terrains.findPath(spawn, village, {
maxSlope: 32, // 度;更缓的斜面步行者可以斜着之字形爬上去
step: 8, // 搜索格子的边长,世界单位
blocked: (x, z) => (waters.shoreAt(x, z) ?? 1) < 0, // 不涉水
});
if (way.state !== 'reached') regenerate(); // 'unreachable'、'exhausted'(用完 maxCells)或 'noGround'

返回的路径在能直线走通的地方被拉直,并给出沿地面的长度。它是用于检查和规划的查询,不适合每帧调用:几百米耗时几毫秒,maxCells 限制长距离搜索,用完时报告“用完了”而不是“没有路”。每帧行走的角色请用导航。

无限地面由生成器生成:一个关于世界位置的函数,在每台机器上都一样。它被要求采样一个区域(世界、种子和版本、以世界单位计的起点、间距和每边样本数),返回那里的高度;如果它自己给地面上色,还返回每个样本四个图层权重。每个区域都是同一个函数在不同点上的读数,所以相邻页共享边上的样本,粗页和细页在样本重合处一致,页生成的先后不改变任何东西。位置是世界的,不是地形的:同一个世界上分开摆放的两个地形是同一片地面。

import { registerTerrainProvider, type TerrainProvider } from 'esengine/terrain';
const planet: TerrainProvider = {
id: 'mygame:planet',
versions: [1, 2], // 存档里的世界可能仍会指名的每个版本
range: (seed, version) => [-200, 900], // 最低与最高高度,GPU 按它编码高度
sample({ world, seed, version, x0, z0, spacing, count }) {
const heights = new Float32Array(count * count);
// ... heights[j * count + i] 位于 (x0 + i * spacing, z0 + j * spacing)
return { heights }; // 也可以是 Promise,来自 worker 或服务器
},
};
registerTerrainProvider(planet);

一个版本永久对应一种算法。存档的世界保留 generatorVersion;算法改变时新增一个版本并保留旧版本,这样建在旧地面上的东西仍立在原处。引擎的噪声是 estella:noise 版本 1。带 heightAt 的生成器能立刻回答单点;没有它时,单点从按最细格子采样的小块里回答。

Planet 是球面上的地面。机制和上面的地面一样,只是下面是一个圆的世界:一个立方体,六个面各是一棵四叉树,每个点都被推到半径上,所以最细的格子在眼睛下面,从远处看星球只是几个粗节点。从地面看和从太空看是同一份地面,中间没有什么要切换。

字段 作用
radius 星球半径,单位米。星球立在它的实体处,不旋转、不缩放。
provider、world、generatorVersion、seed 生成地面的生成器(registerPlanetProvider)、游戏给它起的世界名、版本和种子。这四个相同,各处的地面就相同。
cellSize、detail 地面网格最细的格子,单位米;以及最细的格子往外延伸多远(大于 1 更远)。
layerColor0–3、macroScale、macroVariation 四个图层的颜色(草、土、雪、岩),以及盖在上面的亮度起伏:macroScale 是它的尺寸(米),macroVariation 是它的强度(0 到 1)。
layers 最多四个用来绘制地面的 .esterrainlayer 文件;为空则画四种颜色。
collision、collisionDistance、collisionLayer、friction 与平面地形相同:物体是否与地面碰撞、围绕每个运动物体构建地面的范围、地面的层与摩擦。
gravity 星球对其上物体的吸引力,单位米每二次方秒(表面处);0(默认)表示不吸引。
sea、seaLevel、seaColor、seaOpacity 星球是否有海、海面高出半径的高度、海的颜色,以及它盖住下方景物的程度(0 到 1)。

星球的提供器回答的是方向,不是位置:它拿到从星球中心出发的单位方向和它们所在的半径,给出每个方向上高出半径的高度,如果上色,再给出四个图层权重。球面没有 x 和 z,所以生成器读三维噪声,没有接缝也没有极点。

registerPlanetProvider({
id: 'my-game:planet',
versions: [1],
range: () => [-40, 200],
sample({ seed, radius, directions, count }) {
const heights = new Float32Array(count);
for (let i = 0; i < count; i++) heights[i] = myHeight(seed, radius, directions[i * 3]!, directions[i * 3 + 1]!, directions[i * 3 + 2]!);
return { heights };
},
});

星球回答圆形世界向站在上面的一切提出的问题,通过 PlanetRegistryRes 资源:up(planet, x, y, z) 是背离球心的单位方向,altitude 是高出半径的高度,groundHeight 是生成器自己在某位置下的地面高度(提供器稍后才回答时为 null),groundPoint 是它下面地面上的那个点。nearest(x, y, z) 给出表面最近的星球。设置 gravity(表面处的米每二次方秒),3D 物理就把每个动态刚体拉向最近的、有引力的星球中心:世界唯一的重力从刚体上拿掉,换上星球的,每个固定步长作为一个力施加;世界重力设为 0 时,刚体就是朝星球落下。

生成的世界可以铺在星球上:在 Planet 所在的实体上放一个 surface 设为 Planet 的 WorldGen。立方体的六个面各按米制平面,用和平面世界一样的规则(字段之上的群落、配方、图层)铺设;一个方向由它落在的那个面来规划,放出来的东西被带到星球上,沿着当地的“上”站立。字段是星球自己的:地面高度、坡度、在三维里读的噪声(所以字段越过两个面之间的边没有接缝)、星球每个图层的 paint.<层名>,以及提供器用 fields 命名的字段(比如生态)。群落同时给地面上色(paintGround),画出来的和长出来的是同一张图。seaLevel 是海平面高度,比它低的地面在水下。区域(WorldZone)站在世界里的一个位置上,归它落在的那个面。

地面有碰撞:每个运动的 3D 刚体和角色在 collisionDistance 内的最细节点,会用画地面的同一批采样建成碰撞几何(collision、collisionDistance、collisionLayer、friction,和平面地形一样)。星球的变换只有位置:旋转或缩放过的星球,碰撞仍站在没旋转时的位置。layers 可以写最多四个 .esterrainlayer 文件,和平面地形一样;每层按地面朝向世界坐标轴的程度,在三个投影面上取样,远处混入更宽的一次取样,看不出重复。不写图层时地面就是四种颜色。星球的提供器可以把自己的生成器交给引擎,作为 worker 能加载的模块(workerSource:一段自包含的纯 JavaScript 模块文本,默认导出实现 sample、fields、versions 和 id)。这样世界的铺设和地面上色都在 worker 线程里完成,主线程只负责在结果到达时把它放进世界;不提供时一切仍在主线程上做。

设置 sea 星球就有海:一个半透明、受光的球面,高出半径 seaLevel,低于它的地面站在水里(波浪和岸边泡沫是平面水体的,之后会来到球面)。天空跟着地平线转:给 skyAtmosphere 和 heightFog 填上星球的半径和中心,它们就按相机站着的那片地面算天空和雾,太阳是固定方向,所以一个地方的昼夜取决于它在星球上的位置。

有三种手段能藏住模型和地面之间的那条线,它们都不需要地面知道。

Ground Blend 材质。 builtin:ground-blend 着色器就是 Model 着色器,贴图和参数一个不少,用于岩石、路缘、建筑地基。表面靠近身后的东西时,会淡出成那个像素的画面,所以无论地面用什么绘制(图层、光照、阴影),接缝都会融进去,不需要任何烘焙。它对身后的任何东西都有效,不只是地形。

参数 作用
u_blendDistance 表面比身后的东西高出多少才完全显现,世界单位。这是高度,不是沿视线的距离,所以相机倾斜时带宽基本不变。0 表示硬边。
u_blendNoise、u_blendNoiseScale 边缘的抖动程度和世界尺寸,这样它不是一条尺子画的线。
u_layer2、u_layer2Tint、u_layer2Roughness 第二层:泥土、苔藓、湿石头。
u_layer2Tile 第二层在世界中一次重复的长度,从上方投射,所以跨物体也能对齐。0 用网格自己的 uv。
u_layerReach 第二层从身后的东西向上爬多高,效果类似距离场。
u_vertexBlend 1 让顶点色的红通道绘制第二层,并且不再用顶点色给表面染色。

这种材质和水一样,在拷贝不透明场景之后绘制;场景里有它时,即使没有任何后处理也会拷贝。

道路。 在带 Spline 的实体上加 RoadDecal,它会让实体的 MeshRenderer 沿曲线逐行贴合地面,样条或地面变化时重新生成。两侧边缘在半宽的 edgeFade 比例内淡出,所以要配混合材质。没有地形时,它按曲线本身的位置铺设。

字段 作用
step、across 沿曲线的行距和横向格数。越细越贴合地面和弯道。
widthScale 样条点宽度的倍数。
lift 离地高度。地面在远处画得比查询结果粗,所以从远处看的道路需要大一些。
edgeFade 每侧边缘淡出占半宽的比例。
tile 纹理沿曲线一次重复的长度。

积雪和湿润。 snowCover 后处理效果把雪铺在朝天且上方没有遮挡的表面上,地形和上面的岩石一视同仁,不用改任何材质:u_amount、u_snowLine 和 u_snowLineBand(雪线高度)、u_slopeLimit、u_grainScale。Snow 天气会自动带上它。rain 效果把 u_intensity 设为 0、调高 u_wetness,就是不下雨的湿地面。

一张高度图会被整张读进内存:8193×8193 的地面约 256 MiB。把 tiles 指向一个 .esterrain,地面就按分块存储:每块 513×513 个采样(相邻块共用边),外加一张低精度的总览图。

  • 远处的地面从总览图画,总览图一直在内存里。
  • 视点或运动物体附近才读精细分块;超过 256 MiB 时先放掉最久没用的块。还没读到的地方高度暂时未知,碰撞和查询会等它读到后再算。
  • 在地形雕刻面板里点 拆分为分块,就能把现有地形转换过去。
  • 分块地形照样能雕刻和绘制,但只改已加载的分块:笔刷底下有块还没读到,这一笔就跳过并去读它。改过的块在保存前不会被放掉,保存只写改过的块和总览图。
  • 雕刻面板可以导出整块地面的高度图,也能把一张高度图导入到分块上。选 已加载分块 时不会读任何还没加载的块:导出时没加载的部分从总览图补,导入时只写已加载的块和总览图;选 全部分块 则每块都读、都写。有未保存的改动时不能导入。
  • 分块图只经 .esterrain 引用,不会像 heightmap 那样在打开场景时作为贴图预载、上传。

编辑器里实测(4096 单位见方):8193×8193 的地面按分块打开时占 36 MiB,贴近地面时占 45 MiB。

  • 每个位置只有一个高度。 洞穴、悬垂和孔洞需要体积地形,它是独立的体积地面。
  • Ground Blend 淡出成身后看到的东西。 身后没有贴近的东西时(比如岩石侧面背后是远处的山谷),就没有接缝要藏,也不会藏。紧贴在 Ground Blend 物体身后的墙和地面一样算贴近。两个这样的物体不会互相融合,因为它们都不在对方读取的拷贝里。它需要浮点渲染目标,没有时就是普通的 Model。
  • snowCover 是铺在画面上的。 它没有厚度,不会让边缘变圆,也埋不住石头,并且只作用于相机看到的部分。
  • 碰撞按最细一级。 远处的地面画得比碰撞粗,但碰撞发生的地方看不出来;只有运动的物体在 collisionDistance 内才有可碰的地面。
  • 上色精度就是页的精度。 图层权重和高度一起采样,每格两个,所以近处比这更细的 splatmap 会被平均,远处更粗。
  • 没有洞、洞穴或悬挑。 高度场在每个位置只有一个高度。洞穴和桥是和它放在一起的网格。
  • 只有位置。 地形跟随实体的位置;它的旋转和缩放不被考虑,heightAt 不考虑,抬起地面的着色器也不考虑。让实体保持不旋转、不缩放,用 sizeX、sizeZ 和 height 设定地面大小。
  • 无限地面由它的生成器(provider)生成。 它不接受高度图或 splatmap,它的侵蚀(引擎自带噪声的)也不画泥土。
  • 页每帧只做几个,所以在最初几帧,或视点突然跳远之后,地面会先粗一会儿再变清晰,而不是卡住某一帧。