Skip to content

Instantly share code, notes, and snippets.

@Matt54
Last active October 12, 2024 21:27
Show Gist options
  • Save Matt54/6ac5b40d4b00d49d116340167f5e8a31 to your computer and use it in GitHub Desktop.
Save Matt54/6ac5b40d4b00d49d116340167f5e8a31 to your computer and use it in GitHub Desktop.
A flower petal / leaf looking RealityKit view created from a LowLevelMesh
import RealityKit
import SwiftUI
struct FlowerPetal: View {
@State private var rotationAngle: Float = 0
var body: some View {
RealityView { content in
let leafEntity = try! leafEntity()
content.add(leafEntity)
} update: { content in
if let leafEntity = content.entities.first {
leafEntity.transform.rotation = simd_quatf(angle: rotationAngle, axis: [0, 1, 0])
}
}
.onAppear {
startRotationTimer()
}
}
private func startRotationTimer() {
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { _ in
rotationAngle += 0.01
if rotationAngle >= .pi * 2 {
rotationAngle = 0
}
}
}
func leafEntity() throws -> Entity {
let lowLevelMesh = try leafMesh()
let resource = try MeshResource(from: lowLevelMesh)
var material = PhysicallyBasedMaterial()
material.baseColor.tint = .red
material.blending = .transparent(opacity: 0.75)
material.emissiveIntensity = 2.0
material.opacityThreshold = 0.1
material.metallic = 0.0
material.roughness = 0.375
material.faceCulling = .none
material.clearcoat = .init(floatLiteral: 0.5)
material.clearcoatRoughness = .init(floatLiteral: 1.0)
// add some random red shaded emissive color
if let cgImage = createRandomRedShadeNoiseImage(width: 8, height: 8),
let texture = try? TextureResource(image: cgImage, options: .init(semantic: nil)) {
material.emissiveColor = .init(texture: .init(texture))
}
let modelComponent = ModelComponent(mesh: resource, materials: [material])
let entity = Entity()
entity.name = "Leaf"
entity.components.set(modelComponent)
entity.scale *= scalePreviewFactor
return entity
}
func leafMesh() throws -> LowLevelMesh {
let widthSegments = 30
let heightSegments = 60
let width: Float = 0.5
let height: Float = 1.0
let depth: Float = 0.04
let vertexCount = (widthSegments + 1) * (heightSegments + 1) * 2 + (heightSegments + 1) * 4
let indexCount = widthSegments * heightSegments * 12 + heightSegments * 12
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
// Function to calculate leaf shape
func leafShape(_ v: Float, _ u: Float) -> SIMD3<Float> {
let y = v * height - height / 2
// Adjust curve for rounded ends and asymmetry
let baseCurve = sin(v * .pi)
let topAdjust = pow(1 - v, 2) * 0.3 // Makes top end larger
let bottomAdjust = pow(v, 2) * 0.9 // Makes bottom end smaller
let curve = baseCurve * (1 - topAdjust - bottomAdjust)
let leafWidth = curve * width
let x = (u - 0.5) * leafWidth
// Adjust z-curve for more pronounced curvature
let zCurve = sin(v * .pi) * 0.15
return SIMD3<Float>(x, y, zCurve)
}
// Create top and bottom surfaces
for surface in 0...1 {
for i in 0...heightSegments {
let v = Float(i) / Float(heightSegments)
for j in 0...widthSegments {
let u = Float(j) / Float(widthSegments)
var position = leafShape(v, u)
position.z += surface == 0 ? depth / 2 : -depth / 2
vertices[vertexIndex] = MyVertex(position: position, color: 0xFF00FF00)
vertexIndex += 1
}
}
}
// Create side vertices
for i in 0...heightSegments {
let v = Float(i) / Float(heightSegments)
let leftPosition = leafShape(v, 0)
let rightPosition = leafShape(v, 1)
// Left side
vertices[vertexIndex] = MyVertex(position: leftPosition + SIMD3<Float>(0, 0, depth / 2), color: 0xFF00FF00)
vertexIndex += 1
vertices[vertexIndex] = MyVertex(position: leftPosition + SIMD3<Float>(0, 0, -depth / 2), color: 0xFF00FF00)
vertexIndex += 1
// Right side
vertices[vertexIndex] = MyVertex(position: rightPosition + SIMD3<Float>(0, 0, depth / 2), color: 0xFF00FF00)
vertexIndex += 1
vertices[vertexIndex] = MyVertex(position: rightPosition + SIMD3<Float>(0, 0, -depth / 2), color: 0xFF00FF00)
vertexIndex += 1
}
}
mesh.withUnsafeMutableIndices { rawIndices in
let indices = rawIndices.bindMemory(to: UInt32.self)
var index = 0
let vertsPerSurface = (widthSegments + 1) * (heightSegments + 1)
// Top and bottom surfaces
for surface in 0...1 {
let surfaceOffset = surface * vertsPerSurface
for i in 0..<heightSegments {
for j in 0..<widthSegments {
let a = surfaceOffset + i * (widthSegments + 1) + j
let b = a + 1
let c = surfaceOffset + (i + 1) * (widthSegments + 1) + j
let d = c + 1
if surface == 0 {
indices[index] = UInt32(a)
indices[index + 1] = UInt32(c)
indices[index + 2] = UInt32(b)
indices[index + 3] = UInt32(c)
indices[index + 4] = UInt32(d)
indices[index + 5] = UInt32(b)
} else {
indices[index] = UInt32(a)
indices[index + 1] = UInt32(b)
indices[index + 2] = UInt32(c)
indices[index + 3] = UInt32(c)
indices[index + 4] = UInt32(b)
indices[index + 5] = UInt32(d)
}
index += 6
}
}
}
// Side faces
let sideVertexStart = vertsPerSurface * 2
for i in 0..<heightSegments {
let a = sideVertexStart + i * 4
let b = a + 1
let c = a + 4
let d = c + 1
// Left side
indices[index] = UInt32(a)
indices[index + 1] = UInt32(c)
indices[index + 2] = UInt32(b)
indices[index + 3] = UInt32(b)
indices[index + 4] = UInt32(c)
indices[index + 5] = UInt32(d)
// Right side
indices[index + 6] = UInt32(a + 2)
indices[index + 7] = UInt32(b + 2)
indices[index + 8] = UInt32(c + 2)
indices[index + 9] = UInt32(b + 2)
indices[index + 10] = UInt32(d + 2)
indices[index + 11] = UInt32(c + 2)
index += 12
}
}
let meshBounds = BoundingBox(min: [-width/2, -height/2, -depth/2], max: [width/2, height/2, depth/2])
mesh.parts.replaceAll([
LowLevelMesh.Part(
indexCount: indexCount,
topology: .triangle,
bounds: meshBounds
)
])
return mesh
}
func createRandomRedShadeNoiseImage(width: Int, height: Int) -> 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)
for y in 0..<height {
for x in 0..<width {
let red = UInt32(CGFloat.random(in: 0.75...1) * 255)
let green = UInt32(CGFloat.random(in: 0...0.1) * 255)
let blue = UInt32(CGFloat.random(in: 0...0.3) * 255)
let alpha: UInt32 = 255
let color = (alpha << 24) | (blue << 16) | (green << 8) | red
pixelBuffer[y * width + x] = color
}
}
return context.makeImage()
}
}
#Preview {
FlowerPetal()
}
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
}
}
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