Skip to content

Instantly share code, notes, and snippets.

@Matt54
Last active July 21, 2024 13:17
Show Gist options
  • Save Matt54/e532445d7436a86e1b00ed9260a26dcf to your computer and use it in GitHub Desktop.
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.
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