Skip to content

Commit f70016f

Browse files
committed
[Feature] Added Static Batching support
1 parent 319502a commit f70016f

10 files changed

Lines changed: 390 additions & 18 deletions

File tree

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ let package = Package(
1313
// Use a branch during active development:
1414
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop"),
1515
// Or pin to a release:
16-
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.8.2"),
16+
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.9.0"),
1717
],
1818
targets: [
1919
.executableTarget(

Sources/UntoldEditor/Editor/EditorView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ public struct EditorView: View {
124124
Label("Effects", systemImage: "cube")
125125
}
126126

127+
StaticBatchingView()
128+
.tabItem {
129+
Label("Batching", systemImage: "square.3.layers.3d")
130+
}
131+
127132
InspectorView(
128133
selectionManager: selectionManager,
129134
sceneGraphModel: sceneGraphModel,

Sources/UntoldEditor/Editor/InspectorView.swift

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,9 @@ struct InspectorView: View {
295295

296296
let sortedComponents = sortEntityComponents(componentOption_Editor: mergedComponents)
297297

298+
// Static Batching Section - Show for any entity with renderable hierarchy
299+
StaticBatchingEditorView(entityId: entityId, refreshView: refreshView)
300+
298301
ForEach(sortedComponents, id: \.id) { editor_component in
299302
VStack(alignment: .leading, spacing: 4) {
300303
HStack {
@@ -458,6 +461,102 @@ struct InspectorView: View {
458461
}
459462
*/
460463

464+
// Standalone Static Batching Section
465+
struct StaticBatchingEditorView: View {
466+
let entityId: EntityID
467+
let refreshView: () -> Void
468+
469+
@State private var staticBatchCheckboxState: Bool = false
470+
471+
// Check if entity or any of its children have RenderComponent
472+
private func hasRenderableHierarchy(entityId: EntityID) -> Bool {
473+
// Check self
474+
if hasComponent(entityId: entityId, componentType: RenderComponent.self) {
475+
return true
476+
}
477+
478+
// Check children recursively
479+
let children = getEntityChildren(parentId: entityId)
480+
for child in children {
481+
if hasRenderableHierarchy(entityId: child) {
482+
return true
483+
}
484+
}
485+
486+
return false
487+
}
488+
489+
// Check if entity or any of its children have StaticBatchComponent
490+
private func isMarkedAsStatic(entityId: EntityID) -> Bool {
491+
// Check self
492+
if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) {
493+
return true
494+
}
495+
496+
// Check children recursively
497+
let children = getEntityChildren(parentId: entityId)
498+
for child in children {
499+
if isMarkedAsStatic(entityId: child) {
500+
return true
501+
}
502+
}
503+
504+
return false
505+
}
506+
507+
var body: some View {
508+
// Only show if entity or children have RenderComponent (but not lights)
509+
if hasRenderableHierarchy(entityId: entityId), hasComponent(entityId: entityId, componentType: LightComponent.self) == false {
510+
VStack(alignment: .leading, spacing: 4) {
511+
Text("Static Batching")
512+
.font(.headline)
513+
.frame(maxWidth: .infinity, alignment: .leading)
514+
515+
let hasOwnRenderComponent = hasComponent(entityId: entityId, componentType: RenderComponent.self)
516+
let labelText = hasOwnRenderComponent ? "Mark as Static" : "Mark Children as Static"
517+
let helpText = hasOwnRenderComponent
518+
? "Enable static batching for this entity (combines geometry to reduce draw calls)"
519+
: "Enable static batching for all children of this entity (combines geometry to reduce draw calls)"
520+
521+
Toggle(isOn: Binding(
522+
get: { staticBatchCheckboxState },
523+
set: { isStatic in
524+
if isStatic {
525+
setEntityStaticBatchComponent(entityId: entityId)
526+
} else {
527+
removeEntityStaticBatchComponent(entityId: entityId)
528+
}
529+
staticBatchCheckboxState = isStatic
530+
refreshView()
531+
}
532+
)) {
533+
HStack {
534+
Image(systemName: "square.3.layers.3d")
535+
.foregroundColor(.blue)
536+
Text(labelText)
537+
.font(.callout)
538+
}
539+
}
540+
.padding(.vertical, 6)
541+
.padding(.horizontal, 8)
542+
.background(Color.secondary.opacity(0.05))
543+
.cornerRadius(8)
544+
.help(helpText)
545+
.onAppear {
546+
// Update checkbox state when view appears
547+
staticBatchCheckboxState = isMarkedAsStatic(entityId: entityId)
548+
}
549+
.onChange(of: entityId) { newEntityId in
550+
// Update checkbox state when entity selection changes
551+
staticBatchCheckboxState = isMarkedAsStatic(entityId: newEntityId)
552+
}
553+
}
554+
555+
Divider()
556+
}
557+
}
558+
}
559+
461560
struct RenderingEditorView: View {
462561
let entityId: EntityID
463562
let asset: Asset?
@@ -685,15 +784,33 @@ struct TransformationEditorView: View {
685784
let entityId: EntityID
686785
let refreshView: () -> Void
687786

787+
@State private var showStaticBatchWarning = false
788+
688789
var body: some View {
689790
Text("Transform Properties")
791+
792+
// Warning banner if entity is marked as static
793+
if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) {
794+
HStack {
795+
Image(systemName: "exclamationmark.triangle.fill")
796+
.foregroundColor(.orange)
797+
Text("This entity is marked for static batching. Transforming it will disable batching.")
798+
.font(.caption)
799+
.foregroundColor(.orange)
800+
}
801+
.padding(6)
802+
.background(Color.orange.opacity(0.1))
803+
.cornerRadius(6)
804+
}
805+
690806
let localTransformComponent = scene.get(component: LocalTransformComponent.self, for: entityId)
691807
let position = getLocalPosition(entityId: entityId)
692808
let orientation = simd_float3(localTransformComponent!.rotationX, localTransformComponent!.rotationY, localTransformComponent!.rotationZ)
693809
let scale = getScale(entityId: entityId)
694810
TextInputVectorView(label: "Position", value: Binding(
695811
get: { position },
696812
set: { newPosition in
813+
handleTransformChange()
697814
translateTo(entityId: entityId, position: newPosition)
698815
refreshView()
699816
}
@@ -702,6 +819,7 @@ struct TransformationEditorView: View {
702819
TextInputVectorView(label: "Orientation", value: Binding(
703820
get: { orientation },
704821
set: { newOrientation in
822+
handleTransformChange()
705823
applyAxisRotations(entityId: entityId, axis: newOrientation)
706824
refreshView()
707825
}
@@ -710,11 +828,22 @@ struct TransformationEditorView: View {
710828
TextInputVectorView(label: "Scale", value: Binding(
711829
get: { scale },
712830
set: { newScale in
831+
handleTransformChange()
713832
scaleTo(entityId: entityId, scale: newScale)
714833
refreshView()
715834
}
716835
))
717836
}
837+
838+
private func handleTransformChange() {
839+
if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) {
840+
removeEntityStaticBatchComponent(entityId: entityId)
841+
// Optionally regenerate batches without this entity
842+
if isBatchingEnabled() {
843+
generateBatches()
844+
}
845+
}
846+
}
718847
}
719848

720849
struct AnimationEditorView: View {
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
//
2+
// StaticBatchingView.swift
3+
//
4+
//
5+
// Copyright (C) Untold Engine Studios
6+
// Licensed under the GNU LGPL v3.0 or later.
7+
// See the LICENSE file or <https://www.gnu.org/licenses/> for details.
8+
//
9+
import SwiftUI
10+
import UntoldEngine
11+
12+
@available(macOS 12.0, *)
13+
struct StaticBatchingView: View {
14+
@State private var isBatchingEnabled: Bool = false
15+
@State private var batchCount: Int = 0
16+
@State private var showGenerateSuccess: Bool = false
17+
18+
var body: some View {
19+
VStack(alignment: .leading, spacing: 8) {
20+
// MARK: - Header
21+
22+
HStack(spacing: 6) {
23+
Image(systemName: "square.3.layers.3d")
24+
.foregroundColor(.accentColor)
25+
.font(.system(size: 14))
26+
Text("Static Batching")
27+
.font(.headline)
28+
.foregroundColor(.primary)
29+
}
30+
.padding(.bottom, 6)
31+
32+
Divider()
33+
34+
// MARK: - Enable Batching Toggle
35+
36+
VStack(alignment: .leading, spacing: 6) {
37+
Toggle(isOn: $isBatchingEnabled) {
38+
Label("Enable Batching", systemImage: isBatchingEnabled ? "checkmark.circle.fill" : "circle")
39+
.font(.system(size: 12))
40+
}
41+
.toggleStyle(SwitchToggleStyle())
42+
.scaleEffect(0.85)
43+
.onChange(of: isBatchingEnabled) { _, newValue in
44+
enableBatching(newValue)
45+
}
46+
47+
Text("Enable the batching system globally")
48+
.font(.system(size: 10))
49+
.foregroundColor(.secondary)
50+
}
51+
52+
Divider()
53+
54+
// MARK: - Action Buttons
55+
56+
VStack(spacing: 8) {
57+
// Generate Batches Button
58+
Button(action: {
59+
generateBatches()
60+
updateBatchCount()
61+
showGenerateSuccess = true
62+
63+
// Hide success message after 2 seconds
64+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
65+
showGenerateSuccess = false
66+
}
67+
}) {
68+
HStack(spacing: 6) {
69+
Image(systemName: "square.stack.3d.up.fill")
70+
.foregroundColor(.white)
71+
.font(.system(size: 12))
72+
Text("Generate Batches")
73+
.font(.system(size: 12))
74+
.fontWeight(.semibold)
75+
}
76+
.frame(maxWidth: .infinity)
77+
.padding(.vertical, 6)
78+
.padding(.horizontal, 8)
79+
.background(Color.blue)
80+
.foregroundColor(.white)
81+
.cornerRadius(6)
82+
}
83+
.buttonStyle(PlainButtonStyle())
84+
.disabled(!isBatchingEnabled)
85+
86+
// Clear Batches Button
87+
Button(action: {
88+
clearSceneBatches()
89+
updateBatchCount()
90+
}) {
91+
HStack(spacing: 6) {
92+
Image(systemName: "trash.fill")
93+
.foregroundColor(.white)
94+
.font(.system(size: 12))
95+
Text("Clear Batches")
96+
.font(.system(size: 12))
97+
.fontWeight(.semibold)
98+
}
99+
.frame(maxWidth: .infinity)
100+
.padding(.vertical, 6)
101+
.padding(.horizontal, 8)
102+
.background(Color.red.opacity(0.8))
103+
.foregroundColor(.white)
104+
.cornerRadius(6)
105+
}
106+
.buttonStyle(PlainButtonStyle())
107+
}
108+
109+
// Success message
110+
if showGenerateSuccess {
111+
HStack {
112+
Image(systemName: "checkmark.circle.fill")
113+
.foregroundColor(.green)
114+
Text("Batches generated successfully!")
115+
.font(.system(size: 11))
116+
.foregroundColor(.green)
117+
}
118+
.padding(6)
119+
.background(Color.green.opacity(0.1))
120+
.cornerRadius(6)
121+
.transition(.opacity)
122+
}
123+
124+
Divider()
125+
126+
// MARK: - Info Section
127+
128+
VStack(alignment: .leading, spacing: 6) {
129+
Text("Batch Statistics")
130+
.font(.system(size: 12))
131+
.fontWeight(.semibold)
132+
.foregroundColor(.primary)
133+
134+
HStack {
135+
Text("Active Batches:")
136+
.font(.system(size: 11))
137+
.foregroundColor(.secondary)
138+
Spacer()
139+
Text("\(batchCount)")
140+
.font(.system(size: 11))
141+
.fontWeight(.medium)
142+
.foregroundColor(.primary)
143+
}
144+
}
145+
.padding(.vertical, 4)
146+
147+
Divider()
148+
149+
// MARK: - Help Section
150+
151+
VStack(alignment: .leading, spacing: 4) {
152+
Text("How to use:")
153+
.font(.system(size: 11))
154+
.fontWeight(.semibold)
155+
.foregroundColor(.primary)
156+
157+
Text("1. Mark entities as static in Inspector")
158+
.font(.system(size: 10))
159+
.foregroundColor(.secondary)
160+
161+
Text("2. Enable batching toggle above")
162+
.font(.system(size: 10))
163+
.foregroundColor(.secondary)
164+
165+
Text("3. Click 'Generate Batches'")
166+
.font(.system(size: 10))
167+
.foregroundColor(.secondary)
168+
169+
Text("Note: Moving a static entity will automatically disable its batching.")
170+
.font(.system(size: 9))
171+
.foregroundColor(.orange)
172+
.padding(.top, 4)
173+
}
174+
.padding(.vertical, 4)
175+
176+
Spacer()
177+
}
178+
.padding(8)
179+
.background(Color.secondary.opacity(0.1))
180+
.cornerRadius(8)
181+
.shadow(color: Color.black.opacity(0.1), radius: 3, x: 0, y: 1)
182+
.onAppear {
183+
isBatchingEnabled = UntoldEngine.isBatchingEnabled()
184+
updateBatchCount()
185+
}
186+
}
187+
188+
private func updateBatchCount() {
189+
// Get batch count from BatchingSystem
190+
batchCount = BatchingSystem.shared.batchGroups.count
191+
}
192+
}

0 commit comments

Comments
 (0)