Skip to content

Instantly share code, notes, and snippets.

@Matt54
Last active July 21, 2024 20:06
Show Gist options
  • Save Matt54/848fd10e454fd5df6b03742c6db3a78d to your computer and use it in GitHub Desktop.
Save Matt54/848fd10e454fd5df6b03742c6db3a78d to your computer and use it in GitHub Desktop.
RealityKit View showcasing a SpotLightComponent using a rotating cone inside a box with a transparent front face
import RealityKit
import SwiftUI
struct SpotLightBoxView: View {
@State private var rootEntity: Entity?
@State private var timer: Timer?
@State private var childEntityPosition: SIMD3<Float> = [0, 0, 0]
@State private var childEntityTargetPosition: SIMD3<Float> = [0, 0, 0]
@State private var childEntityMoveSpeed: Float = 0.0005
@State private var rotationAngles: SIMD3<Float> = [0, 0, 0]
@State private var lastRotationUpdateTime = CACurrentMediaTime()
var body: some View {
GeometryReader3D { proxy in
RealityView { content in
let size = content.convert(proxy.frame(in: .local), from: .local, to: .scene)
let entity = try! getRootEntity(boundingBox: size)
content.add(entity)
rootEntity = entity
}
.onAppear { startTimer() }
.onDisappear { stopTimer() }
}
}
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1/120.0, repeats: true) { _ in
let direction = normalize(childEntityTargetPosition - childEntityPosition)
childEntityPosition += direction * childEntityMoveSpeed
// Check if we've reached the target position
if distance(childEntityPosition, childEntityTargetPosition) < childEntityMoveSpeed {
childEntityTargetPosition = generateNewTargetPosition()
}
let currentTime = CACurrentMediaTime()
let frameDuration = currentTime - lastRotationUpdateTime
// Rotate along each axis at a different rate for a wonky roll effect
rotationAngles.x += Float(frameDuration * 1.5)
rotationAngles.y += Float(frameDuration * 1.0)
rotationAngles.z += Float(frameDuration * 0.5)
let rotationX = simd_quatf(angle: rotationAngles.x, axis: [1, 0, 0])
let rotationY = simd_quatf(angle: rotationAngles.y, axis: [0, 1, 0])
let rotationZ = simd_quatf(angle: rotationAngles.z, axis: [0, 0, 1])
// Update child entity position
if let childEntity = rootEntity?.children.first {
childEntity.position = childEntityPosition
childEntity.transform.rotation = rotationX * rotationY * rotationZ
}
lastRotationUpdateTime = currentTime
}
childEntityTargetPosition = generateNewTargetPosition()
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
func generateNewTargetPosition() -> SIMD3<Float> {
let range: Float = 0.2 // Adjust this value to control the range of movement
return SIMD3<Float>(
Float.random(in: -range...range),
Float.random(in: -range...range),
Float.random(in: -range...range)
)
}
func getBoxMeshResource(boundingBox: BoundingBox) throws -> MeshResource {
let minDimension = CGFloat.maximum(CGFloat(boundingBox.minX), CGFloat(boundingBox.minY))
let maxDimension = CGFloat.minimum(CGFloat(boundingBox.maxX), CGFloat(boundingBox.maxY))
let widthIncreaseFactor: CGFloat = 1.0
let graphic = SwiftUI.Path { path in
path.move(to: CGPoint(x: minDimension*widthIncreaseFactor, y: minDimension))
path.addLine(to: CGPoint(x: minDimension*widthIncreaseFactor, y: maxDimension))
path.addLine(to: CGPoint(x: maxDimension*widthIncreaseFactor, y: maxDimension))
path.addLine(to: CGPoint(x: maxDimension*widthIncreaseFactor, y: minDimension))
path.addLine(to: CGPoint(x: minDimension*widthIncreaseFactor, y: minDimension))
path.closeSubpath()
}
var extrusionOptions = MeshResource.ShapeExtrusionOptions()
extrusionOptions.extrusionMethod = .linear(depth: boundingBox.boundingRadius)
extrusionOptions.materialAssignment = .init(front: 0, back: 1, extrusion: 1, frontChamfer: 1, backChamfer: 1)
extrusionOptions.chamferRadius = boundingBox.boundingRadius * 0.1
return try MeshResource(extruding: graphic, extrusionOptions: extrusionOptions)
}
func getRootEntity(boundingBox: BoundingBox) throws -> Entity {
let boxEntity = Entity()
let boxMeshResource = try getBoxMeshResource(boundingBox: boundingBox)
let boxModelComponent = ModelComponent(mesh: boxMeshResource, materials: getMaterialArray())
boxEntity.components.set(boxModelComponent)
let coneAndLightEntity = Entity()
// separate entity for cone to rotate separate from spotlight
let coneEntity = Entity()
let coneMeshResource = MeshResource.generateCone(height: 0.1, radius: 0.05)
// rotate cone to face light
coneEntity.transform.rotation = simd_quatf(angle: .pi*0.5, axis: [1, 0, 0])
let coneModelComponent = ModelComponent(mesh: coneMeshResource, materials: [SimpleMaterial(color: .yellow, isMetallic: true)])
coneEntity.components.set(coneModelComponent)
let spotlightComponent = SpotLightComponent(color: .yellow, intensity: 2500, innerAngleInDegrees: 15, outerAngleInDegrees: 45, attenuationRadius: 0.5, attenuationFalloffExponent: 2.0)
coneAndLightEntity.components.set(spotlightComponent)
coneAndLightEntity.addChild(coneEntity)
boxEntity.addChild(coneAndLightEntity)
boxEntity.scale *= scalePreviewFactor
return boxEntity
}
func getMaterialArray() -> [RealityFoundation.Material] {
var transparentMaterial = UnlitMaterial()
transparentMaterial.color.tint = .init(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
transparentMaterial.blending = .transparent(opacity: 0.0)
transparentMaterial.faceCulling = .none
var reflectiveMaterial = PhysicallyBasedMaterial()
reflectiveMaterial.baseColor.tint = .init(red: 0.25, green: 0.25, blue: 0.25, alpha: 1.0)
reflectiveMaterial.metallic = 0.0
reflectiveMaterial.roughness = 0.0
reflectiveMaterial.faceCulling = .none
return [transparentMaterial, reflectiveMaterial]
}
}
#Preview {
SpotLightBoxView()
}
var isPreview: Bool {
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
var scalePreviewFactor: Float = isPreview ? 0.3 : 1.0
extension BoundingBox {
var minX: Float {
center.x - extents.x*0.5
}
var minY: Float {
center.y - extents.y*0.5
}
var maxX: Float {
center.x + extents.x*0.5
}
var maxY: Float {
center.y + extents.y*0.5
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment