Last active
July 21, 2024 20:06
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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