Terrain Component for Editor

A debug tool has been added that allows you to check which mipmap level is used for occlusion checking, as well as the pixel area.

  • The verification algorithm has also been improved.

5 Likes

An update has been made: terrain rendering is now performed with a single draw call using the new rendering type MultiDrawInstancingAccelerator , which requires support for the GL_ANGLE_multi_draw extension. This improvement does not affect the functionality of previous rendering methods.

@see MultiDrawPatchInstancing.mts

3 Likes

WebGPU demonstrates outstanding performance: rendering the landscape takes only 0.3 ms on an integrated UHD Graphics 730, maintaining a stable 75 fps.

I would not completely trust the GPU timer. Not that it’s miles off, but on WebGPU we can only measure duration of render and compute passes, but nothing else. Compared to WebGL, where the whole frame is measured, including data uploads and stall or similar.

What’s the best way to measure WebGPU performance today? Something like RenderDoc or PIX?

The up to date knowledge we have is here

2 Likes

This likely very much depends on how busy the GPU is. If not busy, it can be fast (3ms), but under heavy load, this will be done after the already submitted rendering is done, and take a lot longer.

1 Like

I hear you … WebGL is very limited here.
Doing it in vertex shader brings disadvantages too … its hard to say if that’s worth the win in many cases, likely only in some specialized cases.

Very cool.
One solution I used in a large scale game on xbox 360 in the past was cpu occlusion. I had large quads (could be triangles too) in the level, and per frame pick few of the near ones - those are likely to provide some occlusion. Then during the CPU culling, I would also do an occlusion test (all this was on a separate thread of course, harder in JS).

Advantage vs gpu occlusion - you can even completely remove the draw call including material uniform setup and other render state.

This is something that could work well for hilly terrain, you could generate some occluders for the larger hills.

1 Like

:rocket: New Rendering Accelerators!

Added PatchIndirectDrawAccelerator (WebGPU only) — a powerful tool that works with hierarchical Z-buffer (HZB) and culls landscape patch rendering on the fly! By testing depth in real-time, it significantly reduces GPU load.

Optimized PatchMultiDrawAccelerator for WebGPU:

  • Removed the circular texture buffer for patch data
  • Now uses u32 firstInstance with packed data — simpler and faster!

Architecture supports Occlusion Queries and Readback HZB Tester:

  • Work together or separately for maximum flexibility
  • Readback HZB Tester: poor on Android, excellent on PC/iPhone

Shadows now support render type selection for even more optimization options!

image

2 Likes

Dear users and developers,

Please be advised that starting July 29, 2026, our library will end support for the first major version of the engine and for any engine versions older than 2.17.0. From that date the minimum supported engine version for the library will be 2.17.0. Engine v1 releases and older engine versions are officially deprecated and will no longer receive security updates, critical bug fixes, or compatibility improvements.

The decision to raise the minimum supported engine version followed careful consideration: we introduced architectural improvements, updated dependencies, and optimized core modules in the library. These changes improved stability and performance but require a more modern engine baseline. Raising the minimum to 2.17.0 enables us to focus on new features, speed up our release cadence, and provide more reliable support for current projects.

If your projects run on engine v1 or any engine versions older than 2.17.0, please prepare a migration plan. Contact our support team if you need help — we will publish migration guides and a changelog of notable differences to ease the process.

Thank you for your understanding and continued support. We remain committed to improving the library and appreciate the community’s contributions.

2 Likes

The demo runs in Chrome using WebGL2 on the PlayCanvas engine. The video showcases our vegetation placement system for terrain surfaces, featuring approximately 300,000 trees in total. The system supports up to 1024 vegetation types simultaneously.

Thanks to an advanced terrain LOD system, the JavaScript execution time in a single-threaded environment is around 1 ms on an AMD Ryzen 5 5600G with Radeon Graphics. This performance is achieved through an optimized data layout: arrays are structured to better fit CPU cache, resulting in faster computations.

At its core, the system uses a hierarchical structure similar to a BVH, partitioning the world into sectors with minimal memory overhead. This structure can be efficiently updated when terrain height is modified via a heightmap editor. Additionally, an occlusion culling system is used to significantly reduce rendering workload.

–

Демо запущено в Chrome с использованием WebGL2 на движке PlayCanvas. В видео демонстрируется наша система размещения растительности на поверхности ландшафта, где общее количество деревьев составляет около 300 000. Система поддерживает до 1024 типов растительности одновременно.

Благодаря продвинутой системе LOD для поверхности, время выполнения на JavaScript в однопоточном режиме составляет около 1 мс на AMD Ryzen 5 5600G with Radeon Graphics. Такой результат достигнут за счёт оптимизированного подхода к хранению данных: структуры массивов лучше укладываются в кэш-память процессора, что ускоряет вычисления.

В основе системы лежит иерархическая структура, аналогичная BVH, которая разбивает мир на сектора с минимальным потреблением памяти. Эта структура быстро обновляется при изменении высоты ландшафта через редактор карты высот. Дополнительно используется система окклюзионного отсечения, позволяющая существенно снизить нагрузку на рендеринг.

1 Like

Update:

After implementing TerrainShape, the engine’s private API has been changed. Injection of a new type via system.implementations is no longer supported. I have released a patch that resolves this using the standard approach: assigning the shape to the collision component followed by reactivating the rigidbody. Please take note of this update. Thank you for your understanding.

We’ve improved shadow performance for terrain object components. Previously, shadows could disappear due to frustum processing; now, shadows are correctly cached for specific areas until the LOD is redefined. Please note that it’s best to avoid spamming objects in areas where shadow casting can cause such artifacts.

You can also disable fade for switching LODs, this will slightly reduce memory consumption and calculation time. (lodFadeTime = 0)


Demo

Nice improvement!

I’ve finally integrated the system directly into the editor :rocket:
I’m opening the first beta test of the landscape system for subscribed users :sunrise_over_mountains:

Most of the functionality has already been ported :white_check_mark:
I’m currently polishing the remaining details :wrench:

So you can already start using it in your projects :video_game:




1 Like

Most of the editor functionality is already implemented. Heightmaps can be imported either from a texture or from chunks. Chunk support enables progressive terrain loading. For this purpose, a new method was introduced:

public loadFromPoint(localPosition: pc.Vec3, radius: number): void

It is especially useful during project startup when you want to load the terrain incrementally.

Multi-user editing is also supported. Each user is assigned a specific area they can modify, while all changes are synchronized across all participants.

A runtime synchronization system has also been added for the launcher. Heightmap changes are automatically synced a few seconds after you stop editing in the editor.

Compatibility and performance have been improved. Overall, the product is already usable, and more features will be added gradually.

As mentioned earlier, I designed a custom chunk format for loading and unloading data to disk or a server. It supports incremental streaming of chunk data: heightmaps and splat-maps, landscape and grass object positions (note that GPU-resident data is not unloaded). When the game runs, you can stream data progressively from the player’s position outward, and mask loading with mechanics like fading fog or similar techniques. Chunk size is configurable in settings so you can tune it for your needs. A chunk manager automates load/unload operations and is integrated with the application’s AssetRegistry.

HeightMapFactorsFragment.ts => .alhf
SplatMapFragment.ts => .alsf


Also added was zView, which limits the visibility of terrain patches at a distance.

Please note that while the chunks are being updated, the editor will display information about the work being done. If you try to close the window, the system will notify you that not all data has been downloaded to disk and that you must wait for it to complete.

I’m happy to share that I’ve improved my object instancing system (trees, rocks, fences, etc.), which now works with both WebGPU and WebGL2.

Key improvements:

  • Merges up to 32 objects into a single draw call
  • Shadow rendering can be merged into just 1 draw call
  • Uses the same merge conditions as batching (based on the same combining logic)

Important note: this is instancing, not batching. Conceptually, it’s closer to dynamic batching, but implemented via instancing.

The implementation uses my OptiPixel library, with instancing built upon the BasicHierarchicalInstancer class. This approach has already reduced the number of draw calls for foliage and stems by a factor of 6: foliage from all LOD levels is combined into one group, and stems from different LOD levels into another. As a result, we get only 2 draw calls for 3 LOD levels and 8 calls for 4‑cascade shadows. The terrain itself renders in 1 call, and its shadows in 4 calls.

2 Likes

This video demos MultiDrawInstancer from terrain-system: GPU multi-draw instancing of objects on terrain.

What you see on screen:

  • one draw call renders 3 LOD levels at once (detailed meshes up close, simplified farther away);
  • the same pass also draws several mesh variants (different models in one object layer);
  • as the camera moves, LODs switch by distance with hysteresis and fade — no separate draw call per mesh type.

How it works:

  • LOD and variant meshes are concatenated into a shared concat-mesh and drawn via a proxy MeshInstance with setMultiDraw / draw commands;
  • up to 16 different primitives in a single multi-draw call;
  • hierarchical quadtree instancer + patch frustum culling;
  • capacity up to 65,535 instances per layer (capacity: 0xffff);
  • custom shader chunks (GLSL / WGSL) for instance indices;
  • WebGL2 and WebGPU support (different instancing buffer layouts).

Typical approach: N variants × M LODs ≈ many draw calls.
Here: different LODs and mesh variants go through one multi-draw pass — less CPU overhead and more stable frames on dense scenes.

Stack: PlayCanvas · TypeScript · GPU multi-draw · hierarchical instancing · LOD

1 Like