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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let package = Package(
// Use a branch during active development:
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop"),
// Or pin to a release:
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.8.2"),
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.9.0"),
],
targets: [
.executableTarget(
Expand Down
5 changes: 5 additions & 0 deletions Sources/UntoldEditor/Editor/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public struct EditorView: View {
Label("Effects", systemImage: "cube")
}

StaticBatchingView()
.tabItem {
Label("Batching", systemImage: "square.3.layers.3d")
}

InspectorView(
selectionManager: selectionManager,
sceneGraphModel: sceneGraphModel,
Expand Down
149 changes: 149 additions & 0 deletions Sources/UntoldEditor/Editor/InspectorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,24 @@ var availableComponents_Editor: [ComponentOption_Editor] = [
}
)
}),

ComponentOption_Editor(
id: getComponentId(for: LODComponent.self),
name: "LOD Component",
type: LODComponent.self,
view: { selectedId, asset, refreshView in
AnyView(
Group {
if let entityId = selectedId {
LODComponentEditorView(entityId: entityId, asset: asset, refreshView: refreshView)
}
}
)
},
onAdd: { entityId in
setEntityLodComponent(entityId: entityId)
}
),
]

// Script Component - controlled by feature flag
Expand Down Expand Up @@ -277,6 +295,9 @@ struct InspectorView: View {

let sortedComponents = sortEntityComponents(componentOption_Editor: mergedComponents)

// Static Batching Section - Show for any entity with renderable hierarchy
StaticBatchingEditorView(entityId: entityId, refreshView: refreshView)

ForEach(sortedComponents, id: \.id) { editor_component in
VStack(alignment: .leading, spacing: 4) {
HStack {
Expand Down Expand Up @@ -407,6 +428,8 @@ struct InspectorView: View {
scene.remove(component: GaussianComponent.self, from: entityId)
} else if key == ObjectIdentifier(ScriptComponent.self) {
scene.remove(component: ScriptComponent.self, from: entityId)
} else if key == ObjectIdentifier(LODComponent.self) {
scene.remove(component: LODComponent.self, from: entityId)
}

refreshView()
Expand Down Expand Up @@ -438,6 +461,102 @@ struct InspectorView: View {
}
*/

// Standalone Static Batching Section
struct StaticBatchingEditorView: View {
let entityId: EntityID
let refreshView: () -> Void

@State private var staticBatchCheckboxState: Bool = false

// Check if entity or any of its children have RenderComponent
private func hasRenderableHierarchy(entityId: EntityID) -> Bool {
// Check self
if hasComponent(entityId: entityId, componentType: RenderComponent.self) {
return true
}

// Check children recursively
let children = getEntityChildren(parentId: entityId)
for child in children {
if hasRenderableHierarchy(entityId: child) {
return true
}
}

return false
}

// Check if entity or any of its children have StaticBatchComponent
private func isMarkedAsStatic(entityId: EntityID) -> Bool {
// Check self
if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) {
return true
}

// Check children recursively
let children = getEntityChildren(parentId: entityId)
for child in children {
if isMarkedAsStatic(entityId: child) {
return true
}
}

return false
}

var body: some View {
// Only show if entity or children have RenderComponent (but not lights)
if hasRenderableHierarchy(entityId: entityId), hasComponent(entityId: entityId, componentType: LightComponent.self) == false {
VStack(alignment: .leading, spacing: 4) {
Text("Static Batching")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)

let hasOwnRenderComponent = hasComponent(entityId: entityId, componentType: RenderComponent.self)
let labelText = hasOwnRenderComponent ? "Mark as Static" : "Mark Children as Static"
let helpText = hasOwnRenderComponent
? "Enable static batching for this entity (combines geometry to reduce draw calls)"
: "Enable static batching for all children of this entity (combines geometry to reduce draw calls)"

Toggle(isOn: Binding(
get: { staticBatchCheckboxState },
set: { isStatic in
if isStatic {
setEntityStaticBatchComponent(entityId: entityId)
} else {
removeEntityStaticBatchComponent(entityId: entityId)
}
staticBatchCheckboxState = isStatic
refreshView()
}
)) {
HStack {
Image(systemName: "square.3.layers.3d")
.foregroundColor(.blue)
Text(labelText)
.font(.callout)
}
}
.padding(.vertical, 6)
.padding(.horizontal, 8)
.background(Color.secondary.opacity(0.05))
.cornerRadius(8)
.help(helpText)
.onAppear {
// Update checkbox state when view appears
staticBatchCheckboxState = isMarkedAsStatic(entityId: entityId)
}
.onChange(of: entityId) { newEntityId in
// Update checkbox state when entity selection changes
staticBatchCheckboxState = isMarkedAsStatic(entityId: newEntityId)
}
}

Divider()
}
}
}

struct RenderingEditorView: View {
let entityId: EntityID
let asset: Asset?
Expand Down Expand Up @@ -665,15 +784,33 @@ struct TransformationEditorView: View {
let entityId: EntityID
let refreshView: () -> Void

@State private var showStaticBatchWarning = false

var body: some View {
Text("Transform Properties")

// Warning banner if entity is marked as static
if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text("This entity is marked for static batching. Transforming it will disable batching.")
.font(.caption)
.foregroundColor(.orange)
}
.padding(6)
.background(Color.orange.opacity(0.1))
.cornerRadius(6)
}

let localTransformComponent = scene.get(component: LocalTransformComponent.self, for: entityId)
let position = getLocalPosition(entityId: entityId)
let orientation = simd_float3(localTransformComponent!.rotationX, localTransformComponent!.rotationY, localTransformComponent!.rotationZ)
let scale = getScale(entityId: entityId)
TextInputVectorView(label: "Position", value: Binding(
get: { position },
set: { newPosition in
handleTransformChange()
translateTo(entityId: entityId, position: newPosition)
refreshView()
}
Expand All @@ -682,6 +819,7 @@ struct TransformationEditorView: View {
TextInputVectorView(label: "Orientation", value: Binding(
get: { orientation },
set: { newOrientation in
handleTransformChange()
applyAxisRotations(entityId: entityId, axis: newOrientation)
refreshView()
}
Expand All @@ -690,11 +828,22 @@ struct TransformationEditorView: View {
TextInputVectorView(label: "Scale", value: Binding(
get: { scale },
set: { newScale in
handleTransformChange()
scaleTo(entityId: entityId, scale: newScale)
refreshView()
}
))
}

private func handleTransformChange() {
if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) {
removeEntityStaticBatchComponent(entityId: entityId)
// Optionally regenerate batches without this entity
if isBatchingEnabled() {
generateBatches()
}
}
}
}

struct AnimationEditorView: View {
Expand Down
Loading
Loading