Skip to content

Instantly share code, notes, and snippets.

@Matt54
Created June 28, 2024 02:57
Show Gist options
  • Save Matt54/42565f55f958ccc21f1bee617be9c2f6 to your computer and use it in GitHub Desktop.
Save Matt54/42565f55f958ccc21f1bee617be9c2f6 to your computer and use it in GitHub Desktop.
LowLevelMesh Sphere with programatically generated gradient texture
import RealityKit
import SwiftUI
struct GradientTextureSphereView: View {
let cgImage: CGImage
@State private var rotationAngle: Float = 0
@State private var previousSize: SIMD3<Float> = .zero
var body: some View {
GeometryReader3D { proxy in
RealityView { content in
let size = content.convert(proxy.frame(in: .local), from: .local, to: .scene).extents
let radius = Float(0.5 * size.x)
let triangleEntity = try! sphereEntity(radius: radius, cgImage: cgImage)
content.add(triangleEntity)
previousSize = size
} update: { content in
let size = content.convert(proxy.frame(in: .local), from: .local, to: .scene).extents
let radius = Float(0.5 * size.x)
if let entity = content.entities.first {
entity.transform.rotation = simd_quatf(angle: rotationAngle, axis: [0, 1, 0])
// only update model component if volume changes
if size != previousSize,
let modelComponent = try? getModelComponent(radius: radius, cgImage: cgImage) {
entity.components.set(modelComponent)
previousSize = size
}
}
}
.onAppear {
startRotationTimer()
}
}
}
private func startRotationTimer() {
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { _ in
rotationAngle += 0.01
if rotationAngle >= .pi * 2 {
rotationAngle = 0
}
}
}
func sphereEntity(radius: Float = 0.5, cgImage: CGImage) throws -> Entity {
let entity = Entity()
entity.name = "Sphere"
let modelComponent = try getModelComponent(radius: radius, cgImage: cgImage)
entity.components.set(modelComponent)
entity.scale *= 0.25
return entity
}
func getModelComponent(radius: Float = 0.5, cgImage: CGImage) throws -> ModelComponent {
let lowLevelMesh = try sphereMesh(radius: radius)
let resource = try MeshResource(from: lowLevelMesh)
var material = UnlitMaterial()
if let texture = try? TextureResource(image: cgImage, options: .init(semantic: nil)) {
material.color.texture = .init(texture)
}
material.faceCulling = .back
return ModelComponent(mesh: resource, materials: [material])
}
func sphereMesh(radius: Float = 0.5) throws -> LowLevelMesh {
let latitudeBands = 30
let longitudeBands = 50
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 // White color for simplicity
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
)
])
return mesh
}
struct MyVertex {
var position: SIMD3<Float> = .zero
var color: UInt32 = .zero
static var vertexAttributes: [LowLevelMesh.Attribute] = [
.init(semantic: .position, format: .float3, offset: MemoryLayout<Self>.offset(of: \.position)!),
.init(semantic: .color, format: .uchar4Normalized_bgra, offset: MemoryLayout<Self>.offset(of: \.color)!)
]
static var vertexLayouts: [LowLevelMesh.Layout] = [
.init(bufferIndex: 0, bufferStride: MemoryLayout<Self>.stride)
]
static var descriptor: LowLevelMesh.Descriptor {
var desc = LowLevelMesh.Descriptor()
desc.vertexAttributes = MyVertex.vertexAttributes
desc.vertexLayouts = MyVertex.vertexLayouts
desc.indexType = .uint32
return desc
}
}
}
#Preview {
GradientTextureSphereView(cgImage: createRadialGradientImage()!)
}
func createRadialGradientImage(width: Int = 500, height: Int = 500) -> CGImage? {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colors: [CGColor] = [UIColor.red.cgColor, UIColor.black.cgColor, UIColor.red.cgColor]
let locations: [CGFloat] = [0.0, 0.5, 0.75]
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations)
else { return nil }
// Create a bitmap context
guard let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: 4 * width,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
return nil
}
let centerPoint = CGPoint(x: CGFloat(width) / 2.0, y: CGFloat(height) / 2.0)
let radius = min(CGFloat(width), CGFloat(height)) / 1.15
context.drawRadialGradient(
gradient,
startCenter: centerPoint,
startRadius: 0,
endCenter: centerPoint,
endRadius: radius,
options: []
)
return context.makeImage()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment