How to setup bone hierarchy for SCNSkinner from glb data? - swift

I'm trying to show a 3d model of a human body scan in SceneKit. The model is stored in a glb file. Because SceneKit does not support the import of that file format I manually read the file in. I'm trying to use the SCNSkinner from SceneKit to rig the model with the Mixamo-rig included in the glb file. The model is correctly shown as long as I don't apply any bone positions unequal (0, 0, 0), rotations or set boneInverseBindTransforms unequal the Identity matrix. But as soon as I apply any of the afore mentioned properties, the model starts to look unnatural.
Here is the code to create the SCNSkinner object:
private func createSCNSkinnerFrom(bgArmature: BgArmature, baseGeometry: SCNGeometry) -> SCNSkinner? {
var joints: [UInt8] = bgArmature.mesh.joints.joined().map { element in UInt8(element) }
var weights: [Float] = bgArmature.mesh.weights.joined().map { element in element }
let bones: [SCNNode] = self.createBoneNodesList(bgRig: bgArmature.rig!)
let boneIndicesData = Data(bytesNoCopy: &joints, count: joints.count * MemoryLayout<UInt8>.size, deallocator: .none)
let boneIndicesGeometrySource = SCNGeometrySource(data: boneIndicesData, semantic: .boneIndices, vectorCount: joints.count/4, usesFloatComponents: false, componentsPerVector: 4, bytesPerComponent: MemoryLayout<UInt8>.size, dataOffset: 0, dataStride: MemoryLayout<UInt8>.size * 4)
let boneWeightsData = Data(bytesNoCopy: &weights, count: weights.count * MemoryLayout<Float>.size, deallocator: .none)
let boneWeightsGeometrySource = SCNGeometrySource(data: boneWeightsData, semantic: .boneWeights, vectorCount: weights.count/4, usesFloatComponents: true, componentsPerVector: 4, bytesPerComponent: MemoryLayout<Float>.size, dataOffset: 0, dataStride: MemoryLayout<Float>.size * 4)
let boneInverseBindTransforms: [NSValue]? = self.createListOfBoneInverseBindTransforms(bgBones: bgArmature.rig!.bones)
let skinner = SCNSkinner(baseGeometry: baseGeometry,
bones: bones,
boneInverseBindTransforms: boneInverseBindTransforms,
boneWeights: boneWeightsGeometrySource,
boneIndices: boneIndicesGeometrySource)
return skinner
}
Here the function to create a bone node and the list of bones:
private func createBoneNodeWithoutChildren(bgBone: BgBone) -> SCNNode {
let bone = SCNNode()
if let name = bgBone.name {
bone.name = name
}
if let translation = bgBone.translation {
bone.simdPosition = SIMD3<Float>(translation[0]!, translation[1]!, translation[2]!)
}
if let rotation = bgBone.rotation {
bone.simdOrientation = simd_quatf(ix: rotation[0]!, iy: rotation[1]!, iz: rotation[2]!, r: rotation[3]!)
}
if let scale = bgBone.scale {
bone.simdScale = SIMD3<Float>(scale[0]!, scale[1]!, scale[2]!)
}
return bone
}
private func createBoneNodesList(bgRig: BgRig) -> [SCNNode] {
var bonesList: [SCNNode] = []
for bone in bgRig.bones {
bonesList.append(self.createBoneNodeWithoutChildren(bgBone: bone))
}
return bonesList
}
The function to create the boneInverseTransforms:
private func createListOfBoneInverseBindTransforms(bgBones: [BgBone]!) -> [NSValue]? {
var boneInverseBindTransforms: [NSValue]? = []
for bone in bgBones {
boneInverseBindTransforms?.append(NSValue(scnMatrix4: bone.inverseBindMatrix!.getSCNMatrix4()))
}
return boneInverseBindTransforms
}
And the BgBone class with the BgMat4 struct:
class BgBone {
var index: Int?
var children: [Int?]?
var name: String?
var rotation: [Float?]?
var scale: [Float?]?
var translation: [Float?]?
var inverseBindMatrix: BgMat4?
init(index: Int? = nil, children: [Int?]? = nil, name: String? = nil, rotation: [Float?]? = nil, scale: [Float?]? = nil, translation: [Float?]? = nil) {
self.index = index
self.children = children
self.name = name
self.rotation = rotation
self.scale = scale
self.translation = translation
}
}
struct BgMat4: sizeable {
var r1c1: Float
var r2c1: Float
var r3c1: Float
var r4c1: Float
var r1c2: Float
var r2c2: Float
var r3c2: Float
var r4c2: Float
var r1c3: Float
var r2c3: Float
var r3c3: Float
var r4c3: Float
var r1c4: Float
var r2c4: Float
var r3c4: Float
var r4c4: Float
init(fromData: Data) {
/// Accessors of matrix type have data stored in column-major order; start of each column MUST be aligned to 4-byte boundaries.
/// Specifically, when ROWS * SIZE_OF_COMPONENT (where ROWS is the number of rows of the matrix) is not a multiple of 4,
/// then (ROWS * SIZE_OF_COMPONENT) % 4 padding bytes MUST be inserted at the end of each column.
self.r1c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 0, as: Float32.self) }
self.r2c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 4, as: Float32.self) }
self.r3c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 8, as: Float32.self) }
self.r4c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 12, as: Float32.self) }
self.r1c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 16, as: Float32.self) }
self.r2c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 20, as: Float32.self) }
self.r3c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 24, as: Float32.self) }
self.r4c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 28, as: Float32.self) }
self.r1c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 32, as: Float32.self) }
self.r2c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 36, as: Float32.self) }
self.r3c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 40, as: Float32.self) }
self.r4c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 44, as: Float32.self) }
self.r1c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 48, as: Float32.self) }
self.r2c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 52, as: Float32.self) }
self.r3c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 56, as: Float32.self) }
self.r4c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 60, as: Float32.self) }
}
func getSCNMatrix4() -> SCNMatrix4 {
return SCNMatrix4(m11: self.r1c1,
m12: self.r2c1,
m13: self.r3c1,
m14: self.r4c1,
m21: self.r1c2,
m22: self.r2c2,
m23: self.r3c2,
m24: self.r4c2,
m31: self.r1c3,
m32: self.r2c3,
m33: self.r3c3,
m34: self.r4c3,
m41: self.r1c4,
m42: self.r2c4,
m43: self.r3c4,
m44: self.r4c4)
}
}
With this code the model looks like this:
With Positions==(0, 0, 0), Rotation(0, 0, 0), bindInverseTransformation=Identity it looks like this:
But with the Positions, Rotations and bindInverseTransformations applied I expect the model to be in the A-Pose (similar to the following image):
What am I doing wrong with my bones (SCNNodes) or skinner (SCNSkinner) object?

Related

Triangle mesh in Metal

I'm trying to create triangle 2D mesh using SwiftUI & Metal. I have lists of vertices[929 items] and indices[1750 items] to draw a mesh. I have used method .drawIndexedPrimitives, but it isn't correct for this purpose. I think this problem should be solved by using MTKMesh, but I don't know how to apply this to 2D mesh with lists of vertices and indices. Please suggest/give some ideas to solve this problem
The code of Renderer Class below
import Foundation
import MetalKit
class Renderer: NSObject, MTKViewDelegate {
var parent: MetalViewComponent
var metalDevice: MTLDevice!
var metalCommandQueue: MTLCommandQueue!
let pipelineState: MTLRenderPipelineState
let vertexBuffer: MTLBuffer?
// let mesh = MTKMesh(mesh: <#T##MDLMesh#>, device: <#T##MTLDevice#>)
let indexBuffer: MTLBuffer?
init(_ parent: MetalViewComponent) {
self.parent = parent
if let metalDevice = MTLCreateSystemDefaultDevice() {
self.metalDevice = metalDevice
}
self.metalCommandQueue = metalDevice.makeCommandQueue()
let pipelineDescriptor = MTLRenderPipelineDescriptor()
let library = metalDevice.makeDefaultLibrary()
pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertexShader")
pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragmentShader")
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
do {
try pipelineState = metalDevice.makeRenderPipelineState(descriptor: pipelineDescriptor)
} catch {
fatalError()
}
let vertices = [Vertex(position: [-3.49195e-17, -0.570297], color: [0, 1, 1, 1]),
Vertex(position: [-0.245024, -0.578218], color: [0, 1, 1, 1]),
Vertex(position: [-0.529488, -0.52307], color: [0, 1, 1, 1]),...
let indices: [UInt32] = [470,469,433,
433,469,432,
470,433,434,
506,469,507,
469,470,507, // from 1 to 928
vertexBuffer = metalDevice.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Vertex>.stride, options: [])!
indexBuffer = metalDevice.makeBuffer(bytes: indices, length: indices.count * MemoryLayout<UInt16>.size, options: [])
super.init()
print("Число вертексов: \(vertices.count)")
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable else {
return
}
let commandBuffer = metalCommandQueue.makeCommandBuffer()
let renderPassDescriptor = view.currentRenderPassDescriptor
renderPassDescriptor?.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 1.0)
renderPassDescriptor?.colorAttachments[0].loadAction = .clear
renderPassDescriptor?.colorAttachments[0].storeAction = .store
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor!)
renderEncoder?.setRenderPipelineState(pipelineState)
renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder?.drawIndexedPrimitives(type: .lineStrip, indexCount: 1750, indexType: .uint32, indexBuffer: indexBuffer!, indexBufferOffset: 0)
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}
}//
This is what's drawing for now
this is what I expect

MTKView always render the background color to red

I want to use MTKView on Mac app. After init the MTKView, it appears red instead of the expected clear color on My laptop is MacBook Pro of macOS Catalina 10.15.2, and it render black background on a iMac of macOS Mojave 10.14.6
I try to exchange the metalView's pixelFormat (MTLPixelFormat.bgra8Unorm) to MTLPixelFormat.rgba16Float or other. the background color is incorrect too.
The MTLTexture create function is MTLTextureDescriptor.texture2DDescriptor(pixelFormat:metalView.colorPixelFormat, width: Int(metalView.frame.size.width), height: Int(metalView.frame.size.height), mipmapped: false)
It could be a problem with the .metal file, but I don't understand it
I have post the project to Github. here
import Cocoa
import MetalKit
import simd
struct Vertex {
var position: vector_float4
var textCoord: vector_float2
init(position: CGPoint, textCoord: CGPoint) {
self.position = [Float(position.x), Float(position.y), 0 ,1]
self.textCoord = [Float(textCoord.x), Float(textCoord.y)]
}
}
class Matrix {
private(set) var m: [Float]
static var identity = Matrix()
private init() {
m = [1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
}
#discardableResult
func translation(x: Float, y: Float, z: Float) -> Matrix {
m[12] = x
m[13] = y
m[14] = z
return self
}
#discardableResult
func scaling(x: Float, y: Float, z: Float) -> Matrix {
m[0] = x
m[5] = y
m[10] = z
return self
}
}
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, MTKViewDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var metalView: MTKView!
private var commandQueue: MTLCommandQueue?
private var pipelineState: MTLRenderPipelineState!
private var render_target_vertex: MTLBuffer!
private var render_target_uniform: MTLBuffer!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
metalView.device = MTLCreateSystemDefaultDevice()
metalView.delegate = self
commandQueue = metalView.device?.makeCommandQueue()
setupTargetUniforms()
do {
try setupPiplineState()
} catch {
fatalError("Metal initialize failed: \(error.localizedDescription)")
}
}
private func setupTargetUniforms() {
let size = metalView.frame.size
let w = size.width, h = size.height
let vertices = [
Vertex(position: CGPoint(x: 0 , y: 0), textCoord: CGPoint(x: 0, y: 0)),
Vertex(position: CGPoint(x: w , y: 0), textCoord: CGPoint(x: 1, y: 0)),
Vertex(position: CGPoint(x: 0 , y: h), textCoord: CGPoint(x: 0, y: 1)),
Vertex(position: CGPoint(x: w , y: h), textCoord: CGPoint(x: 1, y: 1)),
]
render_target_vertex = metalView.device?.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count, options: .cpuCacheModeWriteCombined)
let metrix = Matrix.identity
metrix.scaling(x: 2 / Float(size.width), y: -2 / Float(size.height), z: 1)
metrix.translation(x: -1, y: 1, z: 0)
render_target_uniform = metalView.device?.makeBuffer(bytes: metrix.m, length: MemoryLayout<Float>.size * 16, options: [])
}
private func setupPiplineState() throws {
let library = try metalView.device?.makeDefaultLibrary(bundle: Bundle.main)
let vertex_func = library?.makeFunction(name: "vertex_render_target")
let fragment_func = library?.makeFunction(name: "fragment_render_target")
let rpd = MTLRenderPipelineDescriptor()
rpd.vertexFunction = vertex_func
rpd.fragmentFunction = fragment_func
rpd.colorAttachments[0].pixelFormat = metalView.colorPixelFormat
// rpd.colorAttachments[0].isBlendingEnabled = true
// rpd.colorAttachments[0].alphaBlendOperation = .add
// rpd.colorAttachments[0].rgbBlendOperation = .add
// rpd.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
// rpd.colorAttachments[0].sourceAlphaBlendFactor = .one
// rpd.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
// rpd.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
pipelineState = try metalView.device?.makeRenderPipelineState(descriptor: rpd)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
let renderPassDescriptor = MTLRenderPassDescriptor()
let attachment = renderPassDescriptor.colorAttachments[0]
attachment?.clearColor = metalView.clearColor
attachment?.texture = metalView.currentDrawable?.texture
attachment?.loadAction = .clear
attachment?.storeAction = .store
let commandBuffer = commandQueue?.makeCommandBuffer()
let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
commandEncoder?.setRenderPipelineState(pipelineState)
commandEncoder?.setVertexBuffer(render_target_vertex, offset: 0, index: 0)
commandEncoder?.setVertexBuffer(render_target_uniform, offset: 0, index: 1)
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: metalView.colorPixelFormat,
width: Int(metalView.frame.size.width),
height: Int(metalView.frame.size.height),
mipmapped: false)
textureDescriptor.usage = [.renderTarget, .shaderRead]
let texture = metalView.device?.makeTexture(descriptor: textureDescriptor)
commandEncoder?.setFragmentTexture(texture, index: 0)
commandEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
commandEncoder?.endEncoding()
if let drawable = metalView.currentDrawable {
commandBuffer?.present(drawable)
}
commandBuffer?.commit()
}
}
Could someone help me find the problem?
You're sampling from an uninitialized texture.
In your MetalView's draw method, you bind the texture from the screenTarget variable, which has never been rendered to.
When a Metal texture is created, its contents are undefined. They might be red, black, random noise, or anything else.

Why is the triangle being rendered with rough edges and not smooth edges? Metal, Swift, Xcode

I am using this code to render the "Hello Triangle" triangle. On my iPhone, though, the triangle has very rough edges, not smooth edges, like in the example.
import UIKit
import Metal
import MetalKit
import simd
class MBEMetalView: UIView {
// // // // // MAIN // // // // //
var metalDevice: MTLDevice! = nil
var metalLayer: CAMetalLayer! = nil
var commandQueue: MTLCommandQueue! = nil
var vertexBuffer: MTLBuffer! = nil
var pipelineState: MTLRenderPipelineState! = nil
var displayLink: CADisplayLink! = nil
override class var layerClass : AnyClass {
return CAMetalLayer.self
}
// override func didMoveToWindow() {
// self.redraw()
// }
override func didMoveToSuperview() {
super.didMoveToSuperview()
if self.superview != nil {
self.displayLink = CADisplayLink(target: self, selector: #selector(displayLinkFired))
self.displayLink.add(to: RunLoop.main, forMode: .common)
} else {
self.displayLink.invalidate()
}
}
#objc func displayLinkFired() {
self.redraw()
}
// // // // // INIT // // // // //
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.prepareDeviceLayerAndQueue()
self.makeBuffers()
self.makePipeline()
}
func prepareDeviceLayerAndQueue() {
metalLayer = (self.layer as! CAMetalLayer)
metalDevice = MTLCreateSystemDefaultDevice()
metalLayer.device = metalDevice
metalLayer.pixelFormat = .bgra8Unorm
commandQueue = metalDevice.makeCommandQueue()
}
func makeBuffers() {
var vertices: [MBEVertex] = [
MBEVertex(position: vector_float4(0, 0.5, 0, 1) , color: vector_float4(1, 0, 0, 1)),
MBEVertex(position: vector_float4(-0.5, -0.5, 0, 1) , color: vector_float4(0, 1, 0, 1)),
MBEVertex(position: vector_float4(0.5, -0.5, 0, 1) , color: vector_float4(0, 0, 1, 1))
]
self.vertexBuffer = metalDevice.makeBuffer(bytes: &vertices, length: 56, options: .storageModeShared)
}
func makePipeline() {
guard let library = metalDevice.makeDefaultLibrary() else { print("COULD NOT CREATE LIBRARY") ; return }
guard let vertexFunction = library.makeFunction(name: "vertex_main") else { print("COULD NOT CREATE A VERTEX FUNCTION") ; return }
guard let fragmentFunction = library.makeFunction(name: "fragment_main") else { print("COULD NOT CREATE LIBRARY") ; return }
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat
pipelineState = try? metalDevice.makeRenderPipelineState(descriptor: pipelineDescriptor)
if pipelineState == nil { print("COULD NOT CREATE PIPELINE STATE") ; return }
}
// // // // // FUNCTIONS // // // // //
func redraw() {
guard let drawable = metalLayer.nextDrawable() else { print("COULD NOT CREATE A DRAWABLE") ; return }
let texture = drawable.texture
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].storeAction = .store
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1)
guard let commandBuffer = commandQueue.makeCommandBuffer() else { print("COULD NOT CREATE A COMMAND BUFFER") ; return }
guard let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { print("COULD NOT CREATE AN ENCODER") ; return }
commandEncoder.setRenderPipelineState(pipelineState)
commandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
commandEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
commandEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
// // // // // TYPES // // // // //
struct MBEVertex {
var position: vector_float4
var color: vector_float4
}
}
I have tried to render the triangle a few different times with different methods (sometimes use a MetalKit view from interface builder, sometimes create the view manually)... each time, though, the triangle comes out with rough edges.
The main issue here is that the drawable size of your layer is much smaller than the resolution of your screen. You can get them to match by taking the following steps:
When your Metal view moves to a new superview, update its contentsScale property to match that of the hosting display:
layer.contentsScale = self.window?.screen.scale ?? 1.0
Add a property to your view subclass that computes the ideal drawable size based on the bounds of the view and its scale:
var preferredDrawableSize: CGSize {
return CGSize(width: bounds.size.width * layer.contentsScale,
height: bounds.size.height * layer.contentsScale)
}
Update the drawableSize of your layer when you detect that it doesn't match the computed preferred size:
func redraw() {
if metalLayer.drawableSize != preferredDrawableSize {
metalLayer.drawableSize = preferredDrawableSize
}
...
}
By the way, these days there's really no good reason not to use MTKView for this purpose. It abstracts all of these details for you and is much nicer to work with.

This MVP Metal main.swift shows up blank

So in a main.swift file in your project, you can create a window (and go from there) like this:
let nsapp = NSApplication.shared
let window = NSWindow(
contentRect: NSMakeRect(0, 0, 200, 200),
styleMask: .fullSizeContentView,
backing: NSWindow.BackingStoreType.buffered,
defer: false
)
window.cascadeTopLeft(from:NSMakePoint(20,20))
nsapp.run()
I'm wondering how to do the same thing but with a Metal triangle. I've been looking through github.com/topics/metalkit but the closest thing I've found so far wasn't there but in a gist.
import Cocoa
import MetalKit
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, MTKViewDelegate {
weak var window: NSWindow!
weak var metalView: MTKView!
let device = MTLCreateSystemDefaultDevice()!
var commandQueue: MTLCommandQueue!
var pipelineState: MTLRenderPipelineState!
func applicationDidFinishLaunching(_ aNotification: Notification) {
metalView = MTKView(frame: NSRect(origin: CGPoint.zero, size: window.frame.size), device: device)
metalView.delegate = self
window.contentView = metalView
commandQueue = device.makeCommandQueue()
let shaders = """
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
packed_float3 position;
packed_float3 color;
};
struct VertexOut {
float4 position [[position]];
float4 color;
};
vertex VertexOut vertex_main(device const VertexIn *vertices [[buffer(0)]],
uint vertexId [[vertex_id]]) {
VertexOut out;
out.position = float4(vertices[vertexId].position, 1);
out.color = float4(vertices[vertexId].color, 1);
return out;
}
fragment float4 fragment_main(VertexOut in [[stage_in]]) {
return in.color;
}
"""
do {
let library = try device.makeLibrary(source: shaders, options: nil)
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.colorAttachments[0].pixelFormat = metalView.colorPixelFormat
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_main")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_main")
pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
} catch {}
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }
guard let passDescriptor = view.currentRenderPassDescriptor else { return }
guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor) else { return }
let vertexData: [Float] = [ -0.5, -0.5, 0, 1, 0, 0,
0.5, -0.5, 0, 0, 1, 0,
0, 0.5, 0, 0, 0, 1 ]
encoder.setVertexBytes(vertexData, length: vertexData.count * MemoryLayout<Float>.stride, index: 0)
encoder.setRenderPipelineState(pipelineState)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
encoder.endEncoding()
commandBuffer.present(view.currentDrawable!)
commandBuffer.commit()
}
}
It at least builds an MTKView from scratch. But I'm not sure yet what the minimum viable product is for getting a metal thing working without any controllers, delegates, applications, I'm going to start just doing trial and error to get it working but it's going to probably take a few days and thought it might be helpful for others if someone's already figured this out.
I have combined the two but it isn't rendering anything from what I can tell.
import AVFoundation
import AudioToolbox
import Foundation
import QuartzCore
import Security
import WebKit
import Cocoa
import Metal
import MetalKit
import Swift
let device = MTLCreateSystemDefaultDevice()!
// Our clear color, can be set to any color
let clearColor = MTLClearColor(red: 0.1, green: 0.57, blue: 0.25, alpha: 1)
let nsapp = NSApplication.shared
let appName = ProcessInfo.processInfo.processName
let window = NSWindow(
contentRect: NSMakeRect(0, 0, 1000, 1000),
styleMask: .fullSizeContentView,
backing: NSWindow.BackingStoreType.buffered,
defer: false
)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.title = appName;
window.makeKeyAndOrderFront(nil)
struct Vertex {
var position: float3
var color: float4
}
let view = MTKView(frame: NSRect(origin: CGPoint.zero, size: window.frame.size), device: device)
window.contentView = view
view.device = device
view.colorPixelFormat = .bgra8Unorm
view.clearColor = clearColor
let queue = device.makeCommandQueue()!
var vertexBuffer: MTLBuffer!
var vertices: [Vertex] = [
Vertex(position: float3(0,1,0), color: float4(1,0,0,1)),
Vertex(position: float3(-1,-1,0), color: float4(0,1,0,1)),
Vertex(position: float3(1,-1,0), color: float4(0,0,1,1))
]
let shaders = """
#include <metal_stdlib>
using namespace metal;
// Basic Struct to match our Swift type
// This is what is passed into the Vertex Shader
struct VertexIn {
float3 position;
float4 color;
};
// What is returned by the Vertex Shader
// This is what is passed into the Fragment Shader
struct VertexOut {
float4 position [[ position ]];
float4 color;
};
vertex VertexOut basic_vertex_function(const device VertexIn *vertices [[ buffer(0) ]],
uint vertexID [[ vertex_id ]]) {
VertexOut vOut;
vOut.position = float4(vertices[vertexID].position,1);
vOut.color = vertices[vertexID].color;
return vOut;
}
fragment float4 basic_fragment_function(VertexOut vIn [[ stage_in ]]) {
return vIn.color;
}
"""
let library = try device.makeLibrary(source: shaders, options: nil)
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.vertexFunction = library.makeFunction(name: "basic_vertex_function")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "basic_fragment_function")
let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
vertexBuffer = device.makeBuffer(
bytes: vertices,
length: MemoryLayout<Vertex>.stride * vertices.count,
options: []
)
enum MetalErrors: Error {
case commandBuffer
case passDescriptor
case encoder
}
guard let drawable = view.currentDrawable else { throw MetalErrors.commandBuffer }
guard let commandBuffer = queue.makeCommandBuffer() else { throw MetalErrors.commandBuffer }
guard let passDescriptor = view.currentRenderPassDescriptor else { throw MetalErrors.passDescriptor }
guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor) else { throw MetalErrors.encoder }
nsapp.run()
// let vertexData: [Float] = [ -0.5, -0.5, 0, 1, 0, 0,
// 0.5, -0.5, 0, 0, 1, 0,
// 0, 0.5, 0, 0, 0, 1 ]
encoder.setRenderPipelineState(pipelineState)
// encoder.setVertexBytes(vertexData, length: vertexData.count * MemoryLayout<Float>.stride, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
It's blank for me. I tried following this as well.
This is getting closer.
The main problem here is that NSApplication's run method doesn't return until the app terminates, so your render command encoding never happens. You can subclass MTKView and override its draw method to do your drawing instead:
import Cocoa
import MetalKit
let device = MTLCreateSystemDefaultDevice()!
// Our clear color, can be set to any color
let clearColor = MTLClearColor(red: 0.1, green: 0.57, blue: 0.25, alpha: 1)
let shaders = """
#include <metal_stdlib>
using namespace metal;
// Basic Struct to match our Swift type
// This is what is passed into the Vertex Shader
struct VertexIn {
float3 position;
float4 color;
};
// What is returned by the Vertex Shader
// This is what is passed into the Fragment Shader
struct VertexOut {
float4 position [[ position ]];
float4 color;
};
vertex VertexOut basic_vertex_function(const device VertexIn *vertices [[ buffer(0) ]],
uint vertexID [[ vertex_id ]]) {
VertexOut vOut;
vOut.position = float4(vertices[vertexID].position,1);
vOut.color = vertices[vertexID].color;
return vOut;
}
fragment float4 basic_fragment_function(VertexOut vIn [[ stage_in ]]) {
return vIn.color;
}
"""
let library = try device.makeLibrary(source: shaders, options: nil)
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.vertexFunction = library.makeFunction(name: "basic_vertex_function")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "basic_fragment_function")
let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
struct Vertex {
var position: float3
var color: float4
}
let queue = device.makeCommandQueue()!
var vertexBuffer: MTLBuffer!
var vertices: [Vertex] = [
Vertex(position: float3(0,1,0), color: float4(1,0,0,1)),
Vertex(position: float3(-1,-1,0), color: float4(0,1,0,1)),
Vertex(position: float3(1,-1,0), color: float4(0,0,1,1))
]
vertexBuffer = device.makeBuffer(
bytes: vertices,
length: MemoryLayout<Vertex>.stride * vertices.count,
options: []
)
enum MetalErrors: Error {
case commandBuffer
case passDescriptor
case encoder
}
class MyMTKView : MTKView {
override func draw() {
guard let drawable = currentDrawable else { return }
guard let passDescriptor = currentRenderPassDescriptor else { return }
guard let commandBuffer = queue.makeCommandBuffer() else { return }
guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor) else { return }
encoder.setRenderPipelineState(pipelineState)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0 )
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
let nsapp = NSApplication.shared
let appName = ProcessInfo.processInfo.processName
let window = NSWindow(
contentRect: NSMakeRect(0, 0, 1000, 1000),
styleMask: [.titled, .closable, .resizable],
backing: NSWindow.BackingStoreType.buffered,
defer: false
)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.title = appName;
let view = MyMTKView(frame: NSRect(origin: CGPoint.zero, size: window.frame.size), device: device)
window.contentView = view
view.device = device
view.colorPixelFormat = .bgra8Unorm
view.clearColor = clearColor
window.makeKeyAndOrderFront(nil)
nsapp.run()

Allow PieChartView to hide value line for tiny slices in Swift

I'm building a pie chart by chart iOS framework. I'm able to hide the value and label when the the slice is tiny but I can't hide the value line for tiny slice.
If I add this code on setDataCount()
set.valueLineWidth = 0.0
It will hide all the value line. How to hide it by the size of slice?
#IBOutlet weak var myChart: PieChartView!
var valueColors = [UIColor]()
var dataEntries = [PieChartDataEntry]()
var record = [Record]()
var category = [String]()
var categoryTotal : [Double] = []
var categoryArray : [String] = []
func setDataCount() {
var totalValue = 0.0
for a in categoryTotal {
totalValue += a
}
UserDefaults.standard.set(totalValue, forKey: "totalValue")
valueAndColor()
let set = PieChartDataSet(values: dataEntries, label: nil)
set.colors = valueColors
set.valueLinePart1OffsetPercentage = 0.8
set.valueLinePart1Length = 0.2
set.valueLinePart2Length = 0.4
set.xValuePosition = .outsideSlice
set.yValuePosition = .outsideSlice
set.selectionShift = 0.0
let data = PieChartData(dataSet: set)
let Formatter:ChartFormatter = ChartFormatter()
data.setValueFormatter(Formatter)
data.setValueFont(.systemFont(ofSize: 11, weight: .light))
data.setValueTextColor(.black)
myChart.data = data
myChart.highlightValues(nil)
}
func valueAndColor(){
for i in 0..<categoryArray.count{
let dataEntry = PieChartDataEntry(value: categoryTotal[i], label: categoryArray[i % categoryArray.count])
dataEntries.append(dataEntry)
//I'm using this code to hide the label
let value = categoryTotal[i]
let total = UserDefaults.standard.double(forKey: "totalValue")
var valueToUse = value/total * 100
valueToUse = Double(round(10*valueToUse)/10)
let minNumber = 10.0
if(valueToUse < minNumber) {
dataEntries[i].label = ""
}else{
dataEntries[i].label = categoryArray[i % categoryArray.count]
}
if categoryArray[i] == "吃喝" {
valueColors.append(UIColor.yellow)
}...
}
I'm using this code to hide the value %
public class ChartFormatter: NSObject, IValueFormatter{
public func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
let total = UserDefaults.standard.double(forKey: "totalValue")
var valueToUse = value/total * 100
valueToUse = Double(round(10*valueToUse)/10)
let minNumber = 10.0
if(valueToUse < minNumber) {
return ""
}
else {
let pFormatter = NumberFormatter()
pFormatter.numberStyle = .percent
pFormatter.maximumFractionDigits = 1
pFormatter.multiplier = 1
pFormatter.percentSymbol = " %"
let hideValue = pFormatter.string(from: NSNumber(value: value))
return String(hideValue ?? "0")
}
}
}
Inside the file PieChartRenderer change from this:
if dataSet.valueLineColor != nil
{
context.setStrokeColor(dataSet.valueLineColor!.cgColor)
context.setLineWidth(dataSet.valueLineWidth)
context.move(to: CGPoint(x: pt0.x, y: pt0.y))
context.addLine(to: CGPoint(x: pt1.x, y: pt1.y))
context.addLine(to: CGPoint(x: pt2.x, y: pt2.y))
context.drawPath(using: CGPathDrawingMode.stroke)
}
to this:
if dataSet.valueLineColor != nil
{
if(valueText == "") {
context.setStrokeColor(UIColor.clear.cgColor)
}
else {
context.setStrokeColor(dataSet.valueLineColor!.cgColor)
}
context.setLineWidth(dataSet.valueLineWidth)
context.move(to: CGPoint(x: pt0.x, y: pt0.y))
context.addLine(to: CGPoint(x: pt1.x, y: pt1.y))
context.addLine(to: CGPoint(x: pt2.x, y: pt2.y))
context.drawPath(using: CGPathDrawingMode.stroke)
}
The change basically checks if the valueText is the empty string, and if so it changes the linecolor to a clear color.