Theme:

Performance

Unia is designed from the ground up to handle extremely large scenes and expansive content. Medium-tier devices such as the: Quest 3, Vega 7, Snapdragon 860+ and Apple M1 are used for performance profiling. Unia's performance for large scenes or complex UI's generally better than Godot and Unity, and aims to work on majority of most modern consumer devices - unlike Unreal.

Batching

Unia uses batching, so all default meshes use a single draw call, this helps with raw GPU throughput. All skinned meshes (avatars) by default also use one single draw call.

Steaming

Textures and geometry are incrementally loaded, this helps with handling VRAM usage for large scenes with a lot of content.

Occlusion and frustum culling

Unia uses compute-driven frustum and occlusion culling. Meshes can be marked as static occluders:

object.occluder = true;

Occluders use their full triangle geometry and their world transform at the time they are registered. They should be low-poly, static meshes. Occluder triangles are double-sided; materials and transparency do not affect the depth-only occlusion pass. An object's nearest bounding-box point must be at least one metre behind the occluder before it can be classified as occluded.

The occluder depth texture can be displayed in the top-left corner from the developer console:

occlusionDebug.showDepth = true;

Set occlusionDebug.depthPower = 1 for raw depth values. The default value of 64 applies a display curve that makes distant depth differences easier to see.

Automatic level of detail (LOD)

Unia dynamically adjusts the level of detail on large objects using meshlets. Distant parts of an object are rendered at lower detail automatically.

To disable LOD on a specific object:

object.LOD = false;

Unia uses MeshOptimizer to handle this efficiently.

Upright ring impostors (opt in)

Set model.octahedralImpostor = true on a rigid model root (a Group or Mesh), or use model.userData.octahedralImpostor = true. The outermost opted-in ancestor owns all rigid descendant meshes, even when older child opt-ins are present. Single-mesh opt-in continues to work. For example:

model.octahedralImpostor = true
model.impostorDistance = 35
model.impostorBlendDistance = 15 // optional: overlap from 35 to 50 metres

The renderer captures the complete model into one impostor, including every member's material and transform relative to the root. All members switch together, and the visible group emits one quad even when its root has no renderable geometry. Captures are shared when member geometries, materials, material update revisions, relative transforms and shadow-casting settings match. Original meshes remain available for collision and raycasting. The group stays on geometry until every member's geometry and source textures are ready and the capture has completed.

Distance overrides use the combined group's world bounds centre. A positive impostorBlendDistance keeps both representations visible across that many metres, using complementary four-sample MSAA masks in the opaque and masked passes, including the masked depth prepass. With no blend distance, the immediate switch retains its 5% return margin. A missing or nonpositive distance uses the automatic screen-size threshold. The City scene opts in whole trees and plants at 35 metres. Foliage and plants blend over the next 5 metres; procedural trees blend over the next 15 metres. Groups containing skinned or unregistered meshes, disabled LOD members or partially hidden members stay on original geometry. Child transform/material changes invalidate the capture; call material.update() after material edits. Membership changes and removal release unused atlas slots. Root movement preserves an existing capture; movement during an unfinished bake restarts that bake to keep the views coherent.

Blended source materials are captured as cutouts. Up to 32 distinct model captures are supported; additional groups remain on mesh LOD. Each capture has 64 horizontal views at 5.625-degree intervals, each containing 128 × 128 pixels with a 16-pixel border, packed into a 1280 × 1280 atlas with four mip levels.

Four views bake per frame, one asset at a time. Each view is captured, cropped, filtered, mipmapped and compressed before its 160 × 160 scratch textures are reused. There is no full-atlas compression burst at the end of capture. RGB dilation preserves alpha coverage; mip filtering retains mean coverage. Rendering uses MSAA sample masks or ordered coverage dithering, with opaque output alpha for covered samples.

BC-capable devices retain the BC7 compute encoder. Devices exposing ETC2 use a bounded ETC2 RGBA encoder: individual-mode RGB with two split orientations and up to two modifier tables per half, plus EAC gradual alpha with two multiplier candidates. Uniform blocks skip split/alpha searches. This trades compression quality for bounded encoding work; foliage quality and GPU timings require device testing. Neither path uses CPU readback. Devices supporting neither format use RGBA8.

Final atlas arrays are allocated in four-asset pages on demand, without reallocating or copying existing pages. Empty pages are released when their assets are removed. One compressed page costs about 8.30 MiB (2.08 MiB per asset), versus 33.20 MiB for RGBA8. Shared view scratch and compression storage use under 0.5 MiB; the bake shadow map adds 4 MiB and instance/matrix buffers about 6.25 MiB. With one compressed page, the subsystem therefore allocates roughly 19 MiB, excluding source geometry/textures and driver overhead. Full 32-asset compressed capacity is approximately 77 MiB. These estimates assume four bytes per depth24plus pixel.

The startup log reports the selected format. octahedralImpostorDebug.capture reports format, allocatedLayers, and atlasMiB (final atlas storage only). There are no runtime impostor normal or depth textures.

Runtime draws one upright quad per instance and blends two adjacent azimuth views with two filtered colour samples, wrapping continuously from the last view to the first. Camera elevation does not tilt the quad; the model's authored up axis is preserved. Captures look horizontally at the bounds centre. This is intended for flat-ground scenery viewed near ground level. The existing octahedralImpostor opt-in name is retained for compatibility. It uses ordinary quad depth testing, with no ray traversal, surface reconstruction, dynamic lighting, shadows, AO, fog or wind. Source material lighting, normal mapping, voxel AO, tint, transmission and highlight settings contribute to the baked colour. A reusable 1024 × 1024 depth32float map captures the whole model from the sun direction, with wind frozen and alpha-tested leaf silhouettes. All 64 colour views sample this map with filtered comparisons and a slope-dependent receiver bias. Trunk and foliage shadow one another; authored castShadow = false members do not cast into the capture. This visibility also drives dark tint and transmission attenuation. The shadow map adds 4 MiB, reused across captures (included in the scratch estimate above). Scene shadow cascades and unrelated surrounding objects are not included. Voxel AO is still a snapshot of the available scene AO at capture time, without an AO-readiness gate. Lighting is frozen from the representative instance at capture time; instances sharing a capture also share that baked lighting, even if rotated or moved.

Once an impostor fully replaces its source, all original member meshes are excluded from shadow casting. During the blend, source meshes continue casting their authored shadows. Returning to geometry restores each authored shadow setting. Collision and raycasting still use the original mesh. Impostors do not cast their own shadow.

This simple representation trades surface accuracy for speed: close views can show crossfade ghosting, a flat silhouette or incorrect intersections. Views from above or below cannot be reproduced by the horizontal capture ring. Use distant switch thresholds for production; a 3-metre override is useful for inspection but makes 128-pixel captures conspicuous and increases screen coverage/overdraw.

For impostor diagnostics, inspect octahedralImpostorDebug.stats in the developer console; octahedralImpostorDebug.capture reports the capture layout. Set octahedralImpostorDebug.tint = true to show active impostor surfaces in magenta. Tint is off by default; set it to false to restore the captured appearance. The stats show registered groups, ready captures, visible impostors, and objects waiting on geometry or textures or excluded by atlas capacity.

Enable per-atlas GPU finishing measurements before captures finish:

gpuTiming.enable('impostor')

Sampled views log dilation, mip generation, compression and summed compute milliseconds. octahedralImpostorDebug.viewTiming holds the latest result, including individual passes, the view index, and its layer within an atlas page. These pass-level timestamps require device support for timestamp-query; check gpuTiming.capabilities(). Readback is asynchronous and skips samples if all three readback buffers are busy. The total measures compute passes only, excluding texture copies, capture rendering, CPU encoding/compilation and queue waiting. Existing ready atlases are not remeasured. Disable with gpuTiming.disable('impostor').