Lab 095 – World, Local & Object Space
Shows the movement in World, Local and Object Space.
Background
Michael Temper is teaching visionOS at the University of Applied Sciences Upper Austria. A couple of weeks ago he wrote about this work on LinkedIn. Michael graciously provided some of the labs/examples that he has been building so we can share them with you here. This is the first one of seven. The labs in this series focus on foundational 3D concepts.
Check out the post from Michael
Lab Code
This lab uses the Draw component and system to visualize the pivot point of three coordinate spaces. It also includes an ornament that displays data and UI. You can move the entities directly, or use the UI. Watch how the values on the display change in real time.
enum SelectableCoordinateSpace: String, CaseIterable, Identifiable {
case world = "World"
case local = "Local"
case object = "Object"
var id: Self { self }
}
struct Lab095: View {
@State var box = Entity()
@State var root = Entity()
@State var selectedSpace: SelectableCoordinateSpace = .local
@State var sphere = Entity()
@State var subscriptions = [EventSubscription]()
@State var boxPosition: SIMD3<Float> = [0.2, 0.2, 0.2]
@State var boxObjectOrigin: SIMD3<Float> = .zero
let modelSortGroup = ModelSortGroup.planarUIAlwaysBehind
init() {
registerComponentsAndSystems()
}
func registerComponentsAndSystems() {
DrawComponent.registerComponent()
DrawRuntimeComponent.registerComponent()
DrawSystem.registerSystem()
}
var body: some View {
RealityView { content in
setupBox()
setupRoot()
setupSphere()
sphere.addChild(box)
root.addChild(sphere)
content.add(root)
subscriptions.append(content.subscribe(to: ManipulationEvents.WillEnd.self) { event in
updateBoxPosition(for: selectedSpace)
})
handleSpace(selectedSpace)
}
.onChange(of: sphere.observable.position, { oldValue, newValue in
// Without this listener, the text views won't update when the sphere gets moved via manipulation
})
.ornament(attachmentAnchor: .scene(.topBack)) {
ornamentUI
}
.preferredWindowClippingMargins(.all, 1000)
}
@ViewBuilder
var ornamentUI: some View {
VStack(alignment: .leading, spacing: 10) {
spacePicker
VectorDisplay(title: "Box Position World", vector: sphere.convert(position: box.observable.position, to: nil))
VectorDisplay(title: "Box Position Local", vector: box.observable.position)
VectorDisplay(title: "Box Position Object", vector: sphere.convert(position: box.observable.position, to: box))
Divider()
BoxPositionSliders(
boxPosition: $boxPosition,
selectedSpace: selectedSpace,
box: box,
boxObjectOrigin: $boxObjectOrigin
) {
updateBoxPosition(for: selectedSpace)
}
}
.padding(.horizontal, 40)
.padding(.vertical)
.glassBackgroundEffect()
}
var spacePicker: some View {
HStack {
Text("Space")
.fontWeight(.bold)
Spacer()
Picker("Space", selection: $selectedSpace) {
ForEach(SelectableCoordinateSpace.allCases) { space in
Text(space.rawValue)
.tag(space as SelectableCoordinateSpace)
}
}
.pickerStyle(.menu)
.onChange(of: selectedSpace) { oldValue, newValue in
handleSpace(newValue)
}
}
}
func setupRoot() {
root.components.set(DrawComponent(showAxes: true))
root.components.set(DrawRuntimeComponent())
let spaceLabel = Entity(components: [ViewAttachmentComponent(rootView: Text("World").font(.largeTitle)), BillboardComponent()])
spaceLabel.position.y = -0.035
root.addChild(spaceLabel)
}
func setupSphere() {
var material = UnlitMaterial(color: .cyan)
material.blending = .transparent(opacity: PhysicallyBasedMaterial.Opacity(floatLiteral: 0.8))
sphere = ModelEntity(
mesh: .generateSphere(radius: 0.015),
materials: [material])
sphere.name = "Sphere"
sphere.position = [0.1, 0.1, 0.1]
sphere.orientation = simd_quatf(angle: -.pi / 2, axis: [0, 1, 0])
var manipulationComponent = ManipulationComponent()
manipulationComponent.releaseBehavior = .stay
manipulationComponent.dynamics.scalingBehavior = .none
manipulationComponent.dynamics.inertia = .zero
sphere.components.set(manipulationComponent)
sphere.generateCollisionShapes(recursive: true)
sphere.components.set(InputTargetComponent())
sphere.components.set(HoverEffectComponent())
sphere.components.set(DrawComponent(showAxes: true))
sphere.components.set(DrawRuntimeComponent())
let spaceLabel = Entity(components: [ViewAttachmentComponent(rootView: Text("Local (Object Parent)").font(.largeTitle)), BillboardComponent()])
spaceLabel.position.y = -0.035
sphere.addChild(spaceLabel)
}
func setupBox() {
var material = UnlitMaterial(color: .orange)
material.blending = .transparent(opacity: PhysicallyBasedMaterial.Opacity(floatLiteral: 0.8))
box = ModelEntity(
mesh: .generateBox(size: 0.03),
materials: [material])
box.name = "Box"
box.position = boxPosition
box.orientation = simd_quatf(angle: .pi / 2, axis: [0, 1, 0])
var manipulationComponent = ManipulationComponent()
manipulationComponent.releaseBehavior = .stay
manipulationComponent.dynamics.scalingBehavior = .none
manipulationComponent.dynamics.inertia = .zero
box.components.set(manipulationComponent)
box.generateCollisionShapes(recursive: true)
box.components.set(InputTargetComponent())
box.components.set(HoverEffectComponent())
box.components.set(DrawComponent(showAxes: true))
box.components.set(DrawRuntimeComponent())
let spaceLabel = Entity(components: [ViewAttachmentComponent(rootView: Text("Object").font(.largeTitle)), BillboardComponent()])
spaceLabel.position.y = -0.035
box.addChild(spaceLabel)
}
func handleSpace(_ space: SelectableCoordinateSpace) {
updateBoxPosition(for: space)
}
func updateBoxPosition(for space: SelectableCoordinateSpace) {
switch space {
case .world:
boxPosition = box.position(relativeTo: nil)
case .local:
boxPosition = box.position
case .object:
boxPosition = box.position(relativeTo: box)
boxObjectOrigin = Transform(matrix: box.transformMatrix(relativeTo: nil)).translation
}
}
func setupModelSorting() {
root.drawRuntimeComponent?.axes?.components.set(ModelSortGroupComponent(group: modelSortGroup, order: 0))
sphere.components.set(ModelSortGroupComponent(group: modelSortGroup, order: 0))
box.components.set(ModelSortGroupComponent(group: modelSortGroup, order: 0))
}
}
#Preview(windowStyle: .volumetric) {
Lab002()
}
struct PositionSlider: View {
var label: String
@Binding var value: Float
var range: ClosedRange<Float> = -2...2
var onValueChange: (Float) -> Void
var onEditingEnd: (() -> Void)? = nil
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(label)
.fontWeight(.bold)
Text(String(format: "%.3f", value))
}
Slider(
value: $value,
in: range,
onEditingChanged: { editing in
if !editing {
onEditingEnd?()
}
}
)
.onChange(of: value) { oldValue, newValue in
onValueChange(newValue)
}
}
}
}
enum Axis {
case x
case y
case z
}
struct BoxPositionSliders: View {
@Binding var boxPosition: SIMD3<Float>
var selectedSpace: SelectableCoordinateSpace
var box: Entity
@Binding var boxObjectOrigin: SIMD3<Float>
var onEditingEnd: (() -> Void)? = nil
var body: some View {
VStack {
PositionSlider(label: "Box X Position", value: $boxPosition.x) { newX in
updateBoxPosition(axis: .x, newValue: newX)
} onEditingEnd: {
onEditingEnd?()
}
PositionSlider(label: "Box Y Position", value: $boxPosition.y) { newY in
updateBoxPosition(axis: .y, newValue: newY)
} onEditingEnd: {
onEditingEnd?()
}
PositionSlider(label: "Box Z Position", value: $boxPosition.z) { newZ in
updateBoxPosition(axis: .z, newValue: newZ)
} onEditingEnd: {
onEditingEnd?()
}
}
}
private func updateBoxPosition(axis: Axis, newValue: Float) {
switch selectedSpace {
case .world:
var newPos = boxPosition
switch axis {
case .x: newPos.x = newValue
case .y: newPos.y = newValue
case .z: newPos.z = newValue
}
box.setPosition([newPos.x, newPos.y, newPos.z], relativeTo: nil)
case .local:
switch axis {
case .x: box.position.x = newValue
case .y: box.position.y = newValue
case .z: box.position.z = newValue
}
case .object:
var t = Transform(matrix: box.transformMatrix(relativeTo: nil))
let localAxis: SIMD3<Float>
switch axis {
case .x: localAxis = t.rotation.act(SIMD3<Float>(1, 0, 0))
case .y: localAxis = t.rotation.act(SIMD3<Float>(0, 1, 0))
case .z: localAxis = t.rotation.act(SIMD3<Float>(0, 0, 1))
}
t.translation = boxObjectOrigin + localAxis * newValue
box.setTransformMatrix(t.matrix, relativeTo: nil)
}
}
}Support our work so we can continue to bring you new examples and articles.

Follow Step Into Vision