Last active
July 21, 2024 13:17
-
-
Save Matt54/e532445d7436a86e1b00ed9260a26dcf to your computer and use it in GitHub Desktop.
RealityKit extruded container view made of reflective and transparent materials. Points lights animate inside with their positions represented as spheres.
This file contains 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 PointLightsInsideContainerView: View { | |
@State private var rootEntity: Entity? | |
@State private var timer: Timer? | |
@State private var spherePositions: [String: SIMD3<Float>] = [:] | |
@State private var sphereTargetPositions: [String: SIMD3<Float>] = [:] | |
@State private var rotationAngle: Float = 0 | |
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! getContainerEntity(boundingBox: size) | |
content.add(entity) | |
Task { await addSpheresToEntity(entity) } | |
self.rootEntity = entity | |
} | |
.onAppear { startTimer() } | |
.onDisappear { stopTimer() } | |
} | |
} | |
func startTimer() { | |
timer = Timer.scheduledTimer(withTimeInterval: 1/120.0, repeats: true) { _ in | |
moveSpheres() | |
rotateContainer() | |
} | |
} | |
func stopTimer() { | |
timer?.invalidate() | |
timer = nil | |
} | |
func moveSpheres() { | |
let sphereMoveSpeed: Float = 0.0025 | |
for (id, position) in spherePositions { | |
let direction = normalize(sphereTargetPositions[id]! - position) | |
spherePositions[id]! += direction * sphereMoveSpeed | |
// Check if we've reached the target position | |
if distance(spherePositions[id]!, sphereTargetPositions[id]!) < sphereMoveSpeed { | |
sphereTargetPositions[id]! = generateNewTargetPosition() | |
} | |
// Update sphere entity position | |
if let childEntity = rootEntity?.findEntity(named: id) { | |
childEntity.position = spherePositions[id]! | |
} | |
} | |
} | |
func rotateContainer() { | |
rotationAngle += 0.0025 | |
if rotationAngle >= .pi * 2 { | |
rotationAngle = 0 | |
} | |
rootEntity?.transform.rotation = simd_quatf(angle: rotationAngle, axis: [0, 0, 1]) | |
} | |
// Adjust this method to control the range of sphere/light movement | |
func generateNewTargetPosition() -> SIMD3<Float> { | |
let range: Float = 0.3125 | |
return SIMD3<Float>( | |
Float.random(in: -range...range), | |
Float.random(in: -range...range), | |
Float.random(in: -range...range) | |
) | |
} | |
func getContainerMeshResource(boundingBox: BoundingBox) throws -> MeshResource { | |
let minDimension = CGFloat.maximum(CGFloat(boundingBox.minX), CGFloat(boundingBox.minY)) | |
let maxDimension = CGFloat.minimum(CGFloat(boundingBox.maxX), CGFloat(boundingBox.maxY)) | |
// adjust for different container shapes | |
let numberOfSides: Int = 128 | |
let center = CGPoint(x: CGFloat(boundingBox.center.x), | |
y: CGFloat(boundingBox.center.y)) | |
let radius = maxDimension - minDimension | |
let angleIncrement: CGFloat = 2 * CGFloat.pi / CGFloat(numberOfSides) | |
let graphic = SwiftUI.Path { path in | |
for i in 0..<numberOfSides { | |
let angle = angleIncrement * CGFloat(i) | |
let x = center.x + radius * cos(angle) | |
let y = center.y + radius * sin(angle) | |
if i == 0 { | |
path.move(to: CGPoint(x: x, y: y)) | |
} else { | |
path.addLine(to: CGPoint(x: x, y: y)) | |
} | |
} | |
path.closeSubpath() | |
} | |
var extrusionOptions = MeshResource.ShapeExtrusionOptions() | |
extrusionOptions.extrusionMethod = .linear(depth: boundingBox.boundingRadius) | |
extrusionOptions.materialAssignment = .init(front: 0, back: 0, extrusion: 1, frontChamfer: 1, backChamfer: 1) | |
extrusionOptions.chamferRadius = boundingBox.boundingRadius * 0.3 | |
return try MeshResource(extruding: graphic, extrusionOptions: extrusionOptions) | |
} | |
func getContainerEntity(boundingBox: BoundingBox) throws -> Entity { | |
let containerEntity = Entity() | |
let boxMeshResource = try getContainerMeshResource(boundingBox: boundingBox) | |
let boxModelComponent = ModelComponent(mesh: boxMeshResource, materials: getContainerMaterialArray()) | |
containerEntity.components.set(boxModelComponent) | |
containerEntity.scale *= scalePreviewFactor | |
return containerEntity | |
} | |
func addSpheresToEntity(_ entity: Entity) async { | |
let sphereRootEntity = Entity() | |
// NOTE - a material can have a max of 8 dynamic lights | |
let numberOfSpheres = 8 | |
let baseColor = generateRandomBrightColor() | |
let complementaryColors = generateColorArray(for: baseColor) | |
for _ in 0..<numberOfSpheres { | |
let color = complementaryColors.randomElement()! | |
let sphereEntity = await generateBlendSphereWithPointLight(color: color) | |
let id = UUID().uuidString | |
sphereEntity.name = id | |
sphereEntity.position = SIMD3<Float>( | |
Float.random(in: -0.2...0.2), | |
Float.random(in: -0.2...0.2), | |
Float.random(in: -0.2...0.2) | |
) | |
spherePositions[id] = sphereEntity.position | |
sphereTargetPositions[id] = generateNewTargetPosition() | |
sphereRootEntity.addChild(sphereEntity) | |
} | |
entity.addChild(sphereRootEntity) | |
} | |
func generateBlendSphereWithPointLight(color: UIColor) async -> Entity { | |
let sphereEntity = Entity() | |
let sphereMeshResource = MeshResource.generateSphere(radius: 0.03) | |
let material = await generateAddMaterial(color: color) | |
let sphereModelComponent = ModelComponent(mesh: sphereMeshResource, materials: [material]) | |
let pointLightComponent = PointLightComponent(color: color, intensity: 3500, attenuationRadius: 0.25) | |
sphereEntity.components.set(pointLightComponent) | |
sphereEntity.components.set(sphereModelComponent) | |
return sphereEntity | |
} | |
func getContainerMaterialArray() -> [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.0125) | |
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] | |
} | |
func generateAddMaterial(color: UIColor) async -> PhysicallyBasedMaterial { | |
var descriptor = PhysicallyBasedMaterial.Program.Descriptor() | |
descriptor.blendMode = .add | |
let prog = await PhysicallyBasedMaterial.Program(descriptor: descriptor) | |
var material = PhysicallyBasedMaterial(program: prog) | |
material.baseColor = PhysicallyBasedMaterial.BaseColor(tint: color) | |
material.metallic = 0.0 | |
material.roughness = 1.0 | |
material.blending = .transparent(opacity: 1.0) | |
return material | |
} | |
func generateRandomBrightColor() -> UIColor { | |
let minimumBrightness: CGFloat = 0.7 | |
let hue = CGFloat.random(in: 0...1) | |
let saturation = 1.0 | |
let brightness = CGFloat.random(in: minimumBrightness...1) | |
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0) | |
} | |
func generateColorArray(for color: UIColor) -> [UIColor] { | |
var hue: CGFloat = 0 | |
var saturation: CGFloat = 0 | |
var brightness: CGFloat = 0 | |
var alpha: CGFloat = 0 | |
color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) | |
let complementaryHue1 = (hue + 0.5).truncatingRemainder(dividingBy: 1.0) | |
let complementaryHue2 = (hue + 0.33).truncatingRemainder(dividingBy: 1.0) | |
let complementaryColor1 = UIColor(hue: complementaryHue1, saturation: saturation, brightness: brightness, alpha: alpha) | |
let complementaryColor2 = UIColor(hue: complementaryHue2, saturation: saturation, brightness: brightness, alpha: alpha) | |
return [color, complementaryColor1, complementaryColor2] | |
} | |
} | |
#Preview { | |
PointLightsInsideContainerView() | |
} | |
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