Created
June 28, 2024 02:57
-
-
Save Matt54/42565f55f958ccc21f1bee617be9c2f6 to your computer and use it in GitHub Desktop.
LowLevelMesh Sphere with programatically generated gradient texture
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 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