Created
July 26, 2024 11:20
-
-
Save Matt54/42626f0893f464f07068546025a3d235 to your computer and use it in GitHub Desktop.
RealityView with 6 shadow casting SpotLights beaming through a sphere with many holes inside of an extruded shape 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 SpotLightsShadowGridSphereView: View { | |
| @State private var rootEntity: Entity? | |
| @State private var timer: Timer? | |
| @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! getEntity(boundingBox: size) | |
| content.add(entity) | |
| rootEntity = entity | |
| } | |
| .onAppear { startTimer() } | |
| .onDisappear { stopTimer() } | |
| } | |
| } | |
| func startTimer() { | |
| timer = Timer.scheduledTimer(withTimeInterval: 1/120.0, repeats: true) { _ in | |
| let currentTime = CACurrentMediaTime() | |
| let frameDuration = currentTime - lastRotationUpdateTime | |
| // Wonky roll effect | |
| rotationAngles.y += Float(frameDuration * 1.0) | |
| rotationAngles.x += Float(frameDuration * 0.25) | |
| rotationAngles.z += Float(frameDuration * 0.0625) | |
| 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 rotation | |
| if let childEntity = rootEntity?.children.first { | |
| childEntity.transform.rotation = rotationY | |
| // childEntity.transform.rotation = rotationX * rotationY | |
| // childEntity.transform.rotation = rotationX * rotationY * rotationZ | |
| } | |
| lastRotationUpdateTime = currentTime | |
| } | |
| } | |
| func stopTimer() { | |
| timer?.invalidate() | |
| timer = nil | |
| } | |
| 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.2 | |
| 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 getEntity(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 sphereRoot = Entity() | |
| let sphereMeshResource = MeshResource.generateSphere(radius: 0.075) | |
| let cgImage = createCircularAlphaGridImage(width: 1000, | |
| height: 1000, | |
| numberOfHoles: 1500, | |
| 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 | |
| let sphereModelComponent = ModelComponent(mesh: sphereMeshResource, materials: [material]) | |
| sphereRoot.components.set(sphereModelComponent) | |
| // add red glow entity | |
| let glowingSphereEntity = createGlowingSphereEntity(radius: 0.4) | |
| sphereRoot.addChild(glowingSphereEntity) | |
| // Helper function to create a spot light entity | |
| func createSpotLight(color: UIColor, direction: SIMD3<Float>) -> SpotLight { | |
| let spotLightComponent = SpotLightComponent(color: color, intensity: 5000, innerAngleInDegrees: 90, outerAngleInDegrees: 110, attenuationRadius: 2.0, attenuationFalloffExponent: 2.0) | |
| let shadow = SpotLightComponent.Shadow() | |
| let spotLight = SpotLight() | |
| spotLight.shadow = shadow | |
| spotLight.components.set(spotLightComponent) | |
| // Align the spotlight direction | |
| spotLight.look(at: direction, from: .zero, relativeTo: sphereRoot) | |
| return spotLight | |
| } | |
| // Directions for the spot lights | |
| let directions: [SIMD3<Float>] = [ | |
| [1, 0, 0], // +X | |
| [-1, 0, 0], // -X | |
| [0, 1, 0], // +Y | |
| [0, -1, 0], // -Y | |
| [0, 0, 1], // +Z | |
| [0, 0, -1] // -Z | |
| ] | |
| for direction in directions { | |
| let spotLight = createSpotLight(color: .red, direction: direction) | |
| sphereRoot.addChild(spotLight) | |
| } | |
| boxEntity.addChild(sphereRoot) | |
| 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 = 1.0 | |
| reflectiveMaterial.faceCulling = .none | |
| return [transparentMaterial, reflectiveMaterial] | |
| } | |
| 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() | |
| } | |
| func createGlowingSphereEntity(radius: Float) -> Entity { | |
| let sphereEntity = Entity() | |
| let numSpheres = 100 | |
| for i in 0..<numSpheres { | |
| let fraction = Float(i) / Float(numSpheres) | |
| let sphereRadius = radius * (1.0 - fraction * 1.0) | |
| let opacity = pow(fraction, 2.0) // Quadratic exaggerates effect | |
| let sphere = Entity() | |
| var material = UnlitMaterial() | |
| material.color.tint = .red | |
| material.blending = .transparent(opacity: .init(floatLiteral: opacity)) | |
| material.faceCulling = .back | |
| let sphereMesh = try! MeshResource.generateSpecificSphere(radius: sphereRadius, latitudeBands: 8, longitudeBands: 8) | |
| let modelComponent = ModelComponent(mesh: sphereMesh, materials: [material]) | |
| sphere.components.set(modelComponent) | |
| sphereEntity.addChild(sphere) | |
| } | |
| sphereEntity.scale *= scalePreviewFactor | |
| return sphereEntity | |
| } | |
| } | |
| #Preview { | |
| SpotLightsShadowGridSphereView() | |
| } | |
| // for creating a low poly sphere | |
| static func generateSpecificSphere(radius: Float, latitudeBands: Int = 10, longitudeBands: Int = 10) throws -> MeshResource { | |
| let vertexCount = (latitudeBands + 1) * (longitudeBands + 1) | |
| let indexCount = latitudeBands * longitudeBands * 6 | |
| var desc = MyVertex.descriptor | |
| desc.vertexCapacity = vertexCount | |
| desc.indexCapacity = indexCount | |
| let mesh = try LowLevelMesh(descriptor: desc) | |
| mesh.withUnsafeMutableBytes(bufferIndex: 0) { rawBytes in | |
| let vertices = rawBytes.bindMemory(to: MyVertex.self) | |
| var vertexIndex = 0 | |
| for latNumber in 0...latitudeBands { | |
| let theta = Float(latNumber) * Float.pi / Float(latitudeBands) | |
| let sinTheta = sin(theta) | |
| let cosTheta = cos(theta) | |
| for longNumber in 0...longitudeBands { | |
| let phi = Float(longNumber) * 2 * Float.pi / Float(longitudeBands) | |
| let sinPhi = sin(phi) | |
| let cosPhi = cos(phi) | |
| let x = cosPhi * sinTheta | |
| let y = cosTheta | |
| let z = sinPhi * sinTheta | |
| let position = SIMD3<Float>(x, y, z) * radius | |
| let color = 0xFFFFFFFF | |
| vertices[vertexIndex] = MyVertex(position: position, color: UInt32(color)) | |
| vertexIndex += 1 | |
| } | |
| } | |
| } | |
| mesh.withUnsafeMutableIndices { rawIndices in | |
| let indices = rawIndices.bindMemory(to: UInt32.self) | |
| var index = 0 | |
| for latNumber in 0..<latitudeBands { | |
| for longNumber in 0..<longitudeBands { | |
| let first = (latNumber * (longitudeBands + 1)) + longNumber | |
| let second = first + longitudeBands + 1 | |
| indices[index] = UInt32(first) | |
| indices[index + 1] = UInt32(second) | |
| indices[index + 2] = UInt32(first + 1) | |
| indices[index + 3] = UInt32(second) | |
| indices[index + 4] = UInt32(second + 1) | |
| indices[index + 5] = UInt32(first + 1) | |
| index += 6 | |
| } | |
| } | |
| } | |
| let meshBounds = BoundingBox(min: [-radius, -radius, -radius], max: [radius, radius, radius]) | |
| mesh.parts.replaceAll([ | |
| LowLevelMesh.Part( | |
| indexCount: indexCount, | |
| topology: .triangle, | |
| bounds: meshBounds | |
| ) | |
| ]) | |
| // Print the number of triangles | |
| let triangleCount = indexCount / 3 | |
| print("Number of triangles: \(triangleCount)") | |
| return try MeshResource(from: mesh) | |
| } | |
| 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