Last active
October 12, 2024 21:27
-
-
Save Matt54/6ac5b40d4b00d49d116340167f5e8a31 to your computer and use it in GitHub Desktop.
A flower petal / leaf looking RealityKit view created from a LowLevelMesh
This file contains hidden or 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 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