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.
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.
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
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
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.
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.
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:
Architecture supports Occlusion Queries and Readback HZB Tester:
Shadows now support render type selection for even more optimization options!

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.
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, которая разбивает мир на сектора с минимальным потреблением памяти. Эта структура быстро обновляется при изменении высоты ландшафта через редактор карты высот. Дополнительно используется система окклюзионного отсечения, позволяющая существенно снизить нагрузку на рендеринг.
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)
Nice improvement!
I’ve finally integrated the system directly into the editor ![]()
I’m opening the first beta test of the landscape system for subscribed users ![]()
Most of the functionality has already been ported ![]()
I’m currently polishing the remaining details ![]()
So you can already start using it in your projects ![]()
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:
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.
This video demos MultiDrawInstancer from terrain-system: GPU multi-draw instancing of objects on terrain.
What you see on screen:
How it works:
setMultiDraw / draw commands;capacity: 0xffff);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