Skip to content

Instantly share code, notes, and snippets.

@Matt54
Last active September 30, 2024 23:37
Show Gist options
  • Select an option

  • Save Matt54/9a3a3933611b114af217b227ead62a4c to your computer and use it in GitHub Desktop.

Select an option

Save Matt54/9a3a3933611b114af217b227ead62a4c to your computer and use it in GitHub Desktop.
RealityView with SpotLights beaming their light through a circle grid
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