Last active
September 30, 2024 23:37
-
-
Save Matt54/9a3a3933611b114af217b227ead62a4c to your computer and use it in GitHub Desktop.
RealityView with SpotLights beaming their light through a circle grid
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 ShadowSpotLightsView: 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 createBoxEntity(boundingBox: boundingBox) | |
| let coneEntities = getConeAndPlaneEntities(boundingBox: boundingBox) | |
| coneEntities.forEach( { boxEntity.addChild($0) }) | |
| boxEntity.scale *= scalePreviewFactor | |
| return boxEntity | |
| } | |
| func createBoxEntity(boundingBox: BoundingBox, hasClearBack: Bool = false) 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 graphic = SwiftUI.Path { path in | |
| path.move(to: CGPoint(x: minDimension, y: minDimension)) | |
| path.addLine(to: CGPoint(x: minDimension, y: maxDimension)) | |
| path.addLine(to: CGPoint(x: maxDimension, y: maxDimension)) | |
| path.addLine(to: CGPoint(x: maxDimension, y: minDimension)) | |
| path.addLine(to: CGPoint(x: minDimension, y: minDimension)) | |
| path.closeSubpath() | |
| } | |
| var extrusionOptions = MeshResource.ShapeExtrusionOptions() | |
| extrusionOptions.extrusionMethod = .linear(depth: boundingBox.boundingRadius) | |
| extrusionOptions.materialAssignment = .init(front: 0, back: hasClearBack ? 0 : 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 createPlaneModelComponent(width: Float, height: Float) -> ModelComponent { | |
| let meshResource = MeshResource.generatePlane(width: width, height: height) | |
| let cgImage = createCircularAlphaGridImage(width: 1000, | |
| height: 1000, | |
| numberOfHoles: 1000, | |
| color: .white) | |
| var material = PhysicallyBasedMaterial() | |
| if let texture = try? TextureResource(image: cgImage!, options: .init(semantic: nil)) { | |
| material.baseColor.texture = .init(texture) | |
| } | |
| material.metallic = 0.0 | |
| material.roughness = 0.0 | |
| material.opacityThreshold = 0.5 | |
| material.faceCulling = .none | |
| return ModelComponent(mesh: meshResource, materials: [material]) | |
| } | |
| func getConeAndPlaneEntities(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*4.0), | |
| SIMD3<Float>(coneOffsetDimension, -coneOffsetDimension, -coneOffsetDimension*4.0), | |
| SIMD3<Float>(-coneOffsetDimension, coneOffsetDimension, -coneOffsetDimension*4.0), | |
| SIMD3<Float>(-coneOffsetDimension, -coneOffsetDimension, -coneOffsetDimension*4.0) | |
| ] | |
| var coneEntities: [Entity] = [] | |
| var planeEntities: [Entity] = [] | |
| // save processing by creating only one model component from the large image | |
| let planeModelComponent = createPlaneModelComponent(width: 0.1, height: 0.1) | |
| for (index, color) in colors.enumerated() { | |
| let coneAndLightEntity = createConeAndLightEntity(color: color) | |
| coneAndLightEntity.position = positions[index] | |
| coneEntities.append(coneAndLightEntity) | |
| let plane = Entity() | |
| plane.components.set(planeModelComponent) | |
| plane.position = positions[index] | |
| plane.position.z = plane.position.z - 0.1 | |
| planeEntities.append(plane) | |
| } | |
| let centerWallPoint = SIMD3<Float>(0, 0, -boundingBox.extents.z + boundingBox.boundingRadius * 0.9) | |
| 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 coneEntities { | |
| animateEntity(modelEntity, centerWallPoint: centerWallPoint, wallPositions: wallPositions) | |
| } | |
| for modelEntity in planeEntities { | |
| applyPlaneAnimation(modelEntity, centerWallPoint: centerWallPoint, wallPositions: wallPositions) | |
| } | |
| var entities: [Entity] = [] | |
| entities.append(contentsOf: coneEntities) | |
| entities.append(contentsOf: planeEntities) | |
| return entities | |
| } | |
| func createConeAndLightEntity(color: UIColor) -> Entity { | |
| let coneAndLightEntity = SpotLight() //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: 15, outerAngleInDegrees: 21, attenuationRadius: 2.0, attenuationFalloffExponent: 2.0) | |
| let shadow = SpotLightComponent.Shadow() | |
| coneAndLightEntity.shadow = shadow | |
| coneAndLightEntity.components.set(spotlightComponent) | |
| coneAndLightEntity.components.set(shadow) | |
| coneAndLightEntity.addChild(coneEntity) | |
| return coneAndLightEntity | |
| } | |
| func animateEntity(_ entity: Entity, | |
| centerWallPoint: SIMD3<Float>, | |
| wallPositions: [SIMD3<Float>]) { | |
| var animationResources: [AnimationResource] = [] | |
| // Rotate to face opposite corner | |
| 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)) | |
| // Rotate back to original position | |
| animationResources.append(createAnimationResource(from: initialTransform, to: entity.transform, delay: 0.0, speed: 0.75)) | |
| animationResources.append(createAnimationResource(from: entity.transform, to: entity.transform, delay: 0.0, speed: 0.25)) | |
| animationResources.append(createAnimationResource(from: entity.transform, to: entity.transform, delay: 0.0, speed: 0.5)) | |
| let sequence = try! AnimationResource.sequence(with: animationResources).repeat() | |
| entity.playAnimation(sequence) | |
| } | |
| func applyPlaneAnimation(_ entity: Entity, | |
| centerWallPoint: SIMD3<Float>, | |
| wallPositions: [SIMD3<Float>]) { | |
| var animationResources: [AnimationResource] = [] | |
| // Rotate to face opposite corner | |
| 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)) | |
| // Rotate back to original position | |
| animationResources.append(createAnimationResource(from: initialTransform, to: entity.transform, delay: 0.0, speed: 0.75)) | |
| var nextTransform = entity.transform | |
| nextTransform.translation = nextTransform.translation - SIMD3<Float>(0.0,0,-0.05) | |
| animationResources.append(createAnimationResource(from: entity.transform, to: nextTransform, delay: 0.0, speed: 1.0)) | |
| animationResources.append(createAnimationResource(from: nextTransform, to: entity.transform, delay: 0.0, speed: 1.0)) | |
| var followingTransform = entity.transform | |
| followingTransform.translation = followingTransform.translation - SIMD3<Float>(0.0,0,0.05) | |
| animationResources.append(createAnimationResource(from: entity.transform, to: followingTransform, delay: 0.0, speed: 1.0)) | |
| animationResources.append(createAnimationResource(from: followingTransform, to: entity.transform, delay: 0.0, speed: 1.0)) | |
| var rotationTransform = entity.transform | |
| rotationTransform.rotation = .init(angle: .pi, axis: [0, 0, 1]) | |
| animationResources.append(createAnimationResource(from: entity.transform, to: rotationTransform, delay: 0.0, speed: 1.0)) | |
| animationResources.append(createAnimationResource(from: rotationTransform, to: entity.transform, delay: 0.0, speed: 1.0)) | |
| 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.5 | |
| reflectiveMaterial.faceCulling = .none | |
| return [transparentMaterial, reflectiveMaterial] | |
| } | |
| } | |
| #Preview { | |
| ShadowSpotLightsView() | |
| } | |
| func createCircularAlphaGridImage(width: Int = 2048, height: Int = 1024, numberOfHoles: Int = 800, color: UIColor) -> CGImage? { | |
| let colorSpace = CGColorSpaceCreateDeviceRGB() | |
| guard let context = CGContext( | |
| data: nil, | |
| width: width, | |
| height: height, | |
| bitsPerComponent: 8, | |
| bytesPerRow: 4 * width, | |
| space: colorSpace, | |
| bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | |
| ) else { | |
| return nil | |
| } | |
| guard let data = context.data else { return nil } | |
| let pixelBuffer = data.bindMemory(to: UInt32.self, capacity: width * height) | |
| let baseColor: UInt32 = color == .white ? (255 << 24) | (255 << 16) | (255 << 8) | 255 : (255 << 24) | (0 << 16) | (0 << 8) | 0 | |
| for y in 0..<height { | |
| for x in 0..<width { | |
| pixelBuffer[y * width + x] = baseColor | |
| } | |
| } | |
| let holeRadius = CGFloat(width) * 0.005 | |
| let numberOfColumns = Int(sqrt(Double(numberOfHoles)).rounded(.up)) | |
| let numberOfRows = (numberOfHoles + numberOfColumns - 1) / numberOfColumns | |
| let horizontalSpacing = (CGFloat(width) - holeRadius * 2) / CGFloat(numberOfColumns - 1) | |
| let verticalSpacing = (CGFloat(height) - holeRadius * 2) / CGFloat(numberOfRows - 1) | |
| var holeCount = 0 | |
| for row in 0..<numberOfRows { | |
| for column in 0..<numberOfColumns { | |
| if holeCount >= numberOfHoles { | |
| break | |
| } | |
| let holeX = holeRadius + (horizontalSpacing * CGFloat(column) + horizontalSpacing / 2) | |
| let holeY = holeRadius + (verticalSpacing * CGFloat(row) + verticalSpacing / 2) | |
| for y in Int(holeY - holeRadius)..<Int(holeY + holeRadius) { | |
| for x in Int(holeX - holeRadius)..<Int(holeX + holeRadius) { | |
| let dx = CGFloat(x) - holeX | |
| let dy = CGFloat(y) - holeY | |
| if dx * dx + dy * dy <= holeRadius * holeRadius { | |
| if x >= 0 && x < width && y >= 0 && y < height { | |
| pixelBuffer[y * width + x] = 0 | |
| } | |
| } | |
| } | |
| } | |
| holeCount += 1 | |
| } | |
| } | |
| return context.makeImage() | |
| } | |
| 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