Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,47 @@
# Changelog
## v0.12.11 - 2026-05-13
### 🐞 Fixes
- [Patch] Fix SSAO floating in XR (9a0fba5…)
- [Patch] Fix DoF depth linearization for reverse-Z (Vision Pro) (79512a3…)
- [Patch] Bloom threshold samples full HDR scene, not emissive-only (0652749…)
- [Patch] Fix DoF bokeh aspect ratio — radius now in pixel space (ebd36b0…)
- [Patch] Replace DoF ring sampling with Vogel disc pattern (16 samples) (947fe23…)
- [Patch] Fix chromatic aberration aspect ratio and redundant samples (e9f35bb…)
- [Patch] Fix vignette shape — circular not elliptical on wide screens (1559ede…)
- [Patch] Wider bloom blur — 9-tap Gaussian kernel, 4 passes, radius 6 (6af62db…)
- [Patch] Eliminate redundant texture sample in LookShader (9ca27f0…)
- [Patch] SSAO bilateral blur: linearize depth and thread reverseZ flag (d211b62…)
- [Patch] Rename BloomCompositeShader textures to match their actual slots (8ec208f…)
- [Patch] Fix ColorCorrectionShader parameter mismatch (d72a8db…)
- [Patch] Remove dead commented-out code block from LookShader (39926ac…)
- [Patch] Update RenderGraphBuilderTest for removed geometryPassId param (8e11d5c…)
- [Patch] Fix SSAO intensity and retune preset values (84ddb59…)
- [Patch] Add progress/percentage to exporter cli (e6741a8…)
- [Patch] Fixed demo camera orbit (ea2c571…)
- [Patch] Fix occlusion for transparency meshes in xr (ce811e0…)
- [Patch] Added cache to the native texture loader (f4470d5…)
- [Patch] Implemented cascade shadow mapping (4856e59…)
- [Patch] Updated the build templates (6c49df0…)
- [Patch] Implemented Temporal Anti-Aliasing (dd3abd3…)
- [Patch] Moved loadUntoldScene from build templates (403320f…)
- [Patch] Added a factor to the tile gen script to use area as input (884e051…)
- [Patch] Fix XR mixed-mode HZB culling behind transparent surfaces (8c63977…)
- [Patch] Fix HZB false culling near transparent surfaces (0c0b0df…)
- [Patch] Generate mip map for untold files (3dda8bc…)
- [Patch] Fix z-figthing issues (b3b6535…)
- [Patch] fixed reversed z for depth debug (848800d…)
- [Patch] Fixed grid after doing reversed-z (225b9c1…)
- [Patch] Preserve original mesh names add selective merge via NM_ prefix (361a299…)
### 📚 Docs
- [Docs] Updated static batching docs (4471b07…)
- [Docs] Updated readme (476056e…)
- [Docs] Updated licenses (9dc3f6d…)
- [Docs] Updated documents (136d8a1…)
- [Docs] Updated readme (434274c…)
- [Docs] Moved documentation to use mkdocs (811d032…)
- [Docs] point to stable release (89c56b7…)
- [Docs] Added load scene docs (cd0a8eb…)
- [Docs] Update spatial docs (9885d85…)
## v0.12.10 - 2026-04-29
### 🐞 Fixes
- [Patch] Made fxaa pass public (736ec7b…)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Clone the repository and launch the demo:
```bash
git clone https://github.com/untoldengine/UntoldEngine.git
cd UntoldEngine
git checkout v0.12.10
git checkout v0.12.11
swift run untolddemo
```

Expand Down
2 changes: 1 addition & 1 deletion Sources/DemoGame/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private enum Constants {
static let appVersion = "0.12.10"
static let appVersion = "0.12.11"
static let defaultWindowSize = NSSize(width: 1920, height: 1080)
static let minimumWindowSize = NSSize(width: 640, height: 480)
}
Expand Down
36 changes: 17 additions & 19 deletions Sources/DemoGame/DemoHUD.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
static let panelCornerRadius: CGFloat = 8
static let panelPadding: CGFloat = 12
static let edgePadding: CGFloat = 16
static let compactMinimumPanelHeight: CGFloat = 220
static let controlsPanelWidth: CGFloat = 320
static let sidePanelWidth: CGFloat = 260
static let resolutionPresets: [ResolutionPreset] = [
Expand Down Expand Up @@ -114,32 +113,17 @@
260,
min(Constants.controlsPanelWidth, proxy.size.width - Constants.edgePadding * 2)
)
let controlsHeight = max(0, proxy.size.height - Constants.edgePadding * 2)

ZStack(alignment: .topLeading) {
SceneView(renderer: renderer)

if isCompact {
if areControlsVisible {
ScrollView {
controlsPanel
.padding(Constants.panelPadding)
}
.frame(width: controlsWidth, alignment: .topLeading)
.frame(
maxHeight: max(Constants.compactMinimumPanelHeight, proxy.size.height - Constants.edgePadding * 2),
alignment: .topLeading
)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: Constants.panelCornerRadius))
.padding(Constants.edgePadding)
.onHover { state.isMouseOverControlPanel = $0 }
controlsPanelContainer(width: controlsWidth, maxHeight: controlsHeight)
}
} else {
controlsPanel
.padding(Constants.panelPadding)
.frame(width: Constants.controlsPanelWidth, alignment: .topLeading)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: Constants.panelCornerRadius))
.padding(Constants.edgePadding)
.onHover { state.isMouseOverControlPanel = $0 }
controlsPanelContainer(width: Constants.controlsPanelWidth, maxHeight: controlsHeight)
}

if isCompact {
Expand Down Expand Up @@ -367,6 +351,20 @@
}
}

private func controlsPanelContainer(width: CGFloat, maxHeight: CGFloat) -> some View {
ScrollView(.vertical) {
controlsPanel
.padding(Constants.panelPadding)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.scrollIndicators(.visible)
.frame(width: width, alignment: .topLeading)
.frame(maxHeight: maxHeight, alignment: .topLeading)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: Constants.panelCornerRadius))
.padding(Constants.edgePadding)
.onHover { state.isMouseOverControlPanel = $0 }
}

private var sidePanel: some View {
HStack {
Spacer()
Expand Down
2 changes: 1 addition & 1 deletion Sources/Sandbox/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private enum Constants {
static let appVersion = "0.12.10"
static let appVersion = "0.12.11"
static let windowSize = NSSize(width: 1600, height: 900)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/UntoldEngine/Renderer/UntoldEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public class UntoldRenderer: NSObject, MTKViewDelegate {

CameraSystem.shared.activeCamera = gameCamera

Logger.log(message: "Untold Engine Starting. Version 0.12.10")
Logger.log(message: "Untold Engine Starting. Version 0.12.11")
}

public func initSizeableResources() {
Expand Down
2 changes: 1 addition & 1 deletion docs/API/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Clone the repository and launch the demo:
```bash
git clone https://github.com/untoldengine/UntoldEngine.git
cd UntoldEngine
git checkout v0.12.10
git checkout v0.12.11
swift run untolddemo
```

Expand Down
19 changes: 19 additions & 0 deletions docs/API/UsingTheExporter.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ Expected output layout:

The manifest stores relative runtime paths so it remains portable across machines, repos, and app bundles.

## Selective Merging With The NM_ Prefix

When `MERGE_BY_MATERIAL` is enabled (the default), objects that share the same material within a tile are joined into a single mesh entity before export. This reduces draw calls significantly, but means multiple original objects collapse into one exported entity — losing their individual names.

If you need certain objects to remain as separate identifiable entities (for example, to support tap-to-select workflows or per-object JSON lookups at runtime), prefix their name in Blender with `NM_`.

Objects whose name starts with `NM_` are excluded from the merge step and exported individually, preserving their original name in the `.untold` file. All other objects are still merged normally.

Example naming in Blender:

- `NM_Pipe_001` — exported as its own entity, name survives into `.untold`
- `NM_LightFixture_A` — exported as its own entity
- `Wall_North` — merged with other same-material walls, one entity for the group
- `Door_Main` — merged with same-material doors

This lets you keep background geometry (walls, floors, ceilings) optimized while still being able to identify and interact with specific objects at runtime:

To change the prefix or disable selective merging, edit `NO_MERGE_PREFIX` at the top of `scripts/tilestreamingpartition.py`. Set it to `""` to merge all objects regardless of name.

## Optimization Workflows

After exporting assets, use [Optimizations](Optimizations.md) for optional
Expand Down
25 changes: 23 additions & 2 deletions scripts/tilestreamingpartition.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ def append_worker_progress(progress_file, event):
# tile. No visual effect. Requires BAKE_WORLD_TRANSFORMS.
MERGE_BY_MATERIAL = True

# Objects whose original name starts with this prefix are never merged, even
# when MERGE_BY_MATERIAL is True. They are exported as individual entities and
# retain their original name in the .untold file. Set to "" to disable.
# Example: name an object "NM_Pipe_001" in Blender to keep it separate.
NO_MERGE_PREFIX = "NM_"

# Clip tolerance at tile boundaries.
# for objects at large world coordinates (e.g. buildings at x=1500).
SPLIT_CLIP_EPSILON = 1e-4
Expand Down Expand Up @@ -1606,6 +1612,7 @@ def duplicate_objects_to_scene(source_objects, temp_scene, bake_world=True):
new_obj = src_obj.copy()
if src_obj.data:
new_obj.data = src_obj.data.copy()
new_obj["mesh_original_name"] = src_obj.name
src_collection.objects.link(new_obj)

if bake_world:
Expand Down Expand Up @@ -2062,6 +2069,7 @@ def split_objects_by_material(objects, temp_scene):
p.material_index = 0

new_obj = bpy.data.objects.new(f"{obj.name}_mat{mat_idx}", new_mesh)
new_obj["mesh_original_name"] = obj.get("mesh_original_name", obj.name)
new_obj.matrix_world = obj.matrix_world.copy()
temp_scene.collection.objects.link(new_obj)
result.append(new_obj)
Expand All @@ -2083,21 +2091,30 @@ def merge_objects_by_material(objects, temp_scene):
is performed at the mesh-data level instead of via bpy.ops.object.join() so
stale object transform caches cannot double-apply importer axis corrections.

Objects whose original name starts with NO_MERGE_PREFIX are passed through
untouched so they keep their own entity name in the exported file.

Returns the reduced object list.
"""
if len(objects) <= 1:
return objects

groups = {}
non_mesh = []
protected = []
for obj in objects:
if obj.type != 'MESH' or obj.data is None:
non_mesh.append(obj)
continue
if NO_MERGE_PREFIX:
original_name = obj.get("mesh_original_name", obj.name)
if original_name.startswith(NO_MERGE_PREFIX):
protected.append(obj)
continue
key = material_merge_key(obj)
groups.setdefault(key, []).append(obj)

result = list(non_mesh)
result = list(non_mesh) + protected
total_mesh_input = sum(len(g) for g in groups.values())
total_merged_away = 0
for key, group in groups.items():
Expand All @@ -2113,6 +2130,8 @@ def merge_objects_by_material(objects, temp_scene):
print(f" Warning: mesh merge failed ({len(group)} objects): {ex}")
result.extend(group)

if protected:
print(f" Mesh merge: {len(protected)} protected object(s) skipped (prefix '{NO_MERGE_PREFIX}')")
if total_merged_away > 0:
print(f" Mesh merge: {total_mesh_input} → {total_mesh_input - total_merged_away} "
f"objects ({total_merged_away} eliminated)")
Expand Down Expand Up @@ -2890,6 +2909,7 @@ def _config_snapshot() -> dict:
"SOURCE_ORIENTATION": SOURCE_ORIENTATION,
"CLIP_LOCAL_MESHES": CLIP_LOCAL_MESHES,
"MERGE_BY_MATERIAL": MERGE_BY_MATERIAL,
"NO_MERGE_PREFIX": NO_MERGE_PREFIX,
"BAKE_WORLD_TRANSFORMS": BAKE_WORLD_TRANSFORMS,
"SPLIT_CLIP_EPSILON": SPLIT_CLIP_EPSILON,
"DEBUG_AABB_ONLY": DEBUG_AABB_ONLY,
Expand All @@ -2900,13 +2920,14 @@ def _config_snapshot() -> dict:
def _apply_bundle_config(cfg: dict) -> None:
"""Restore config globals in a worker process from a bundle dict."""
global EXPORT_FORMAT, CONVERT_ORIENTATION, SOURCE_ORIENTATION
global CLIP_LOCAL_MESHES, MERGE_BY_MATERIAL, BAKE_WORLD_TRANSFORMS
global CLIP_LOCAL_MESHES, MERGE_BY_MATERIAL, NO_MERGE_PREFIX, BAKE_WORLD_TRANSFORMS
global SPLIT_CLIP_EPSILON, DEBUG_AABB_ONLY, COMPRESS_GEOMETRY
EXPORT_FORMAT = cfg.get("EXPORT_FORMAT", EXPORT_FORMAT)
CONVERT_ORIENTATION = cfg.get("CONVERT_ORIENTATION", CONVERT_ORIENTATION)
SOURCE_ORIENTATION = cfg.get("SOURCE_ORIENTATION", SOURCE_ORIENTATION)
CLIP_LOCAL_MESHES = cfg.get("CLIP_LOCAL_MESHES", CLIP_LOCAL_MESHES)
MERGE_BY_MATERIAL = cfg.get("MERGE_BY_MATERIAL", MERGE_BY_MATERIAL)
NO_MERGE_PREFIX = cfg.get("NO_MERGE_PREFIX", NO_MERGE_PREFIX)
BAKE_WORLD_TRANSFORMS = cfg.get("BAKE_WORLD_TRANSFORMS", BAKE_WORLD_TRANSFORMS)
SPLIT_CLIP_EPSILON = cfg.get("SPLIT_CLIP_EPSILON", SPLIT_CLIP_EPSILON)
DEBUG_AABB_ONLY = cfg.get("DEBUG_AABB_ONLY", DEBUG_AABB_ONLY)
Expand Down
6 changes: 3 additions & 3 deletions scripts/untoldexplorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2201,7 +2201,7 @@ def _extract_mesh_numpy(mesh_object: object, mesh_data: object, asset_path: Path
)

return ExportedMesh(
entity_name=mesh_object.name,
entity_name=mesh_object.get("mesh_original_name") or mesh_object.name,
parent_entity_name=getattr(getattr(mesh_object, "parent", None), "name", None),
mesh_name=mesh_object.data.name or mesh_object.name,
local_transform_rows=local_transform_rows,
Expand Down Expand Up @@ -2384,7 +2384,7 @@ def extract_mesh_object(
) if _validate else None

return ExportedMesh(
entity_name=mesh_object.name,
entity_name=mesh_object.get("mesh_original_name") or mesh_object.name,
parent_entity_name=getattr(getattr(mesh_object, "parent", None), "name", None),
mesh_name=mesh_object.data.name or mesh_object.name,
local_transform_rows=local_transform_rows,
Expand Down Expand Up @@ -2615,7 +2615,7 @@ def aggregate_world_corners(obj: object) -> list[tuple[float, float, float]]:
parent_entity_name = parent.name if parent is not None and parent.as_pointer() in export_object_ids else None
nodes.append(
ExportedNode(
entity_name=obj.name,
entity_name=obj.get("mesh_original_name") or obj.name,
parent_entity_name=parent_entity_name,
local_transform_rows=local_transform_rows,
local_bounds=local_bounds,
Expand Down
Loading