Created
July 22, 2024 04:00
-
-
Save Matt54/f92b7af21ef090d3e71c3fad446bbcb3 to your computer and use it in GitHub Desktop.
RealityView playing an animation sequence which blends spotlights inside of an extruded box
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 SpotLightsColorBlendBoxView: View { | |
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) | |
} | |
} | |
} | |
func getRootEntity(boundingBox: BoundingBox) throws -> Entity { | |
let boxEntity = try getBoxEntity(boundingBox: boundingBox) | |
let coneEntities = getConeEntities(boundingBox: boundingBox) | |
coneEntities.forEach( { boxEntity.addChild($0) }) | |
boxEntity.scale *= scalePreviewFactor | |
return boxEntity | |
} | |
func getBoxEntity(boundingBox: BoundingBox) throws -> Entity { | |
let boxEntity = Entity() | |
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 | |
let meshResource = try MeshResource(extruding: graphic, extrusionOptions: extrusionOptions) | |
let modelComponent = ModelComponent(mesh: meshResource, materials: getMaterialArray()) | |
boxEntity.components.set(modelComponent) | |
return boxEntity | |
} | |
func getConeEntities(boundingBox: BoundingBox) -> [Entity] { | |
let coneOffsetDimension = Float.maximum(boundingBox.minX, boundingBox.minY) * 0.5 | |
let colors: [UIColor] = [.green, .blue, .yellow, .red] | |
let positions: [SIMD3<Float>] = [ | |
SIMD3<Float>(coneOffsetDimension, coneOffsetDimension, -coneOffsetDimension*2.0), | |
SIMD3<Float>(coneOffsetDimension, -coneOffsetDimension, -coneOffsetDimension*2.0), | |
SIMD3<Float>(-coneOffsetDimension, coneOffsetDimension, -coneOffsetDimension*2.0), | |
SIMD3<Float>(-coneOffsetDimension, -coneOffsetDimension, -coneOffsetDimension*2.0) | |
] | |
var entities: [Entity] = [] | |
for (index, color) in colors.enumerated() { | |
let coneAndLightEntity = createConeAndLightEntity(color: color) | |
coneAndLightEntity.position = positions[index] | |
entities.append(coneAndLightEntity) | |
} | |
let centerWallPoint = SIMD3<Float>(0, 0, -boundingBox.extents.z + boundingBox.boundingRadius * 0.1) | |
let wallPositions: [SIMD3<Float>] = [ | |
SIMD3<Float>(coneOffsetDimension, coneOffsetDimension, coneOffsetDimension*2.0), | |
SIMD3<Float>(coneOffsetDimension, -coneOffsetDimension, coneOffsetDimension*2.0), | |
SIMD3<Float>(-coneOffsetDimension, coneOffsetDimension, coneOffsetDimension*2.0), | |
SIMD3<Float>(-coneOffsetDimension, -coneOffsetDimension, coneOffsetDimension*2.0) | |
] | |
for modelEntity in entities { | |
animateEntity(modelEntity, centerWallPoint: centerWallPoint, wallPositions: wallPositions) | |
} | |
return entities | |
} | |
func createConeAndLightEntity(color: UIColor) -> Entity { | |
let coneAndLightEntity = Entity() | |
let coneEntity = Entity() | |
let coneMeshResource = MeshResource.generateCone(height: 0.1, radius: 0.05) | |
coneEntity.transform.rotation = simd_quatf(angle: .pi * 0.5, axis: [1, 0, 0]) | |
let coneModelComponent = ModelComponent(mesh: coneMeshResource, materials: [SimpleMaterial(color: color, isMetallic: true)]) | |
coneEntity.components.set(coneModelComponent) | |
let spotlightComponent = SpotLightComponent(color: color, intensity: 5000, innerAngleInDegrees: 5, outerAngleInDegrees: 28, attenuationRadius: 0.5, attenuationFalloffExponent: 2.0) | |
coneAndLightEntity.components.set(spotlightComponent) | |
coneAndLightEntity.addChild(coneEntity) | |
return coneAndLightEntity | |
} | |
func animateEntity(_ entity: Entity, | |
centerWallPoint: SIMD3<Float>, | |
wallPositions: [SIMD3<Float>]) { | |
var animationResources: [AnimationResource] = [] | |
// Initial rotation to face backWallPoint | |
let centerWallDirection = normalize(centerWallPoint - entity.position) | |
let initialRotation = simd_quatf(from: [0, 0, -1], to: centerWallDirection) | |
var initialTransform = entity.transform | |
initialTransform.rotation = initialRotation | |
animationResources.append(createAnimationResource(from: entity.transform, to: initialTransform, delay: 0.25, speed: 0.75)) | |
// Hold facing the backWallPoint | |
animationResources.append(createAnimationResource(from: initialTransform, to: initialTransform, delay: 0.0, speed: 3.0)) | |
// Rotations to face each wall position | |
var previousTransform = initialTransform | |
for position in wallPositions { | |
let nextDirection = normalize(position - entity.position) | |
let nextRotation = simd_quatf(from: [0, 0, -1], to: nextDirection) | |
var nextTransform = previousTransform | |
nextTransform.rotation = nextRotation | |
animationResources.append(createAnimationResource(from: previousTransform, to: nextTransform, delay: 0.0, speed: 1.5)) | |
previousTransform = nextTransform | |
} | |
// Rotate back to face the backWallPoint | |
animationResources.append(createAnimationResource(from: previousTransform, to: initialTransform, delay: 0.0, speed: 1.5)) | |
// Final rotation back to the initial position | |
animationResources.append(createAnimationResource(from: initialTransform, to: entity.transform, delay: 0.0, speed: 0.75)) | |
let sequence = try! AnimationResource.sequence(with: animationResources).repeat() | |
entity.playAnimation(sequence) | |
} | |
func createAnimationResource(from: Transform, to: Transform, delay: Double, speed: Float) -> AnimationResource { | |
let animationDefinition = FromToByAnimation(from: from, to: to, bindTarget: .transform) | |
let animationViewDefinition = AnimationView(source: animationDefinition, delay: delay, speed: speed) | |
return try! AnimationResource.generate(with: animationViewDefinition) | |
} | |
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 = 1.0 | |
reflectiveMaterial.roughness = 0.7 | |
reflectiveMaterial.faceCulling = .none | |
return [transparentMaterial, reflectiveMaterial] | |
} | |
} | |
#Preview { | |
SpotLightsColorBlendBoxView() | |
} | |
var isPreview: Bool { | |
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" | |
} | |
var scalePreviewFactor: Float = isPreview ? 0.3 : 1.0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment