Skip to content

Instantly share code, notes, and snippets.

@Matt54
Created July 26, 2024 11:20
Show Gist options
  • Select an option

  • Save Matt54/42626f0893f464f07068546025a3d235 to your computer and use it in GitHub Desktop.

Select an option

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
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