├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Creamy3D │ ├── ColorRGB.swift │ ├── CreamyView.swift │ ├── Logger.swift │ ├── Materials │ ├── ColorMaterial.swift │ ├── FresnelMaterial.swift │ ├── MatcapMaterial.swift │ ├── Material+Blend.swift │ ├── Material.swift │ └── TextureMaterial.swift │ ├── MetalView.swift │ ├── Objects │ ├── EmptyObject.swift │ ├── Mesh.swift │ └── Object.swift │ ├── Renderer.swift │ ├── Scene │ ├── Camera.swift │ ├── MaterialFunctions │ │ ├── ColorMaterialFunction.swift │ │ ├── FresnelMaterialFunction.swift │ │ ├── MatcapMaterialFunction.swift │ │ ├── MaterialFunction.swift │ │ └── TextureMaterialFunction.swift │ ├── MaterialState.swift │ ├── MeshLoader │ │ ├── CubeMeshLoader.swift │ │ ├── MeshLoader.swift │ │ ├── ModelMeshLoader.swift │ │ └── SphereMeshLoader.swift │ ├── MeshNode.swift │ └── Projection.swift │ └── Shaders │ ├── color_material.metal │ ├── common.metal │ ├── fresnel_material.metal │ ├── matcap_material.metal │ ├── material_common.h │ └── texture_material.metal └── Tests └── Creamy3DTests └── Creamy3DTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-14 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Select Xcode 20 | run: sudo xcode-select -s /Applications/Xcode_16.0.app 21 | - name: Build 22 | run: swift build -v 23 | - name: Run tests 24 | run: swift test -v 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Oleksii Oliinyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Creamy3D", 8 | platforms: [.iOS(.v16), .macOS(.v13)], 9 | products: [ 10 | .library( 11 | name: "Creamy3D", 12 | targets: ["Creamy3D"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "Creamy3D", 17 | resources: [.process("Shaders/")] 18 | ), 19 | .testTarget( 20 | name: "Creamy3DTests", 21 | dependencies: ["Creamy3D"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creamy3D 2 | 3 | ## 🌟 Intro 4 | Creamy 3D is a library that allows seamless integration of simple 3D objects into your SwiftUI projects. Spice up your app's UI with interactive icons and 3D visuals. Its material system draws inspiration from Spline.design. 5 | 6 | ## 🖥️ Code Example 7 | ```Swift 8 | CreamyView { 9 | Mesh(source: .stl("order")) { 10 | MatcapMaterial(name: "matcap") 11 | } 12 | .resizable() 13 | .scaledToFit() 14 | } 15 | ``` 16 | 17 | ## 🎞 Video example 18 | ![ezgif com-video-to-gif](https://github.com/alex566/Creamy3D/assets/7542506/4722124f-75a4-4eae-898f-d925e1f850f3) 19 | 20 | ## 🛠️ How to Use 21 | To infuse a model into your scene, simply start with CreamyView. This view adopts the size of its parent container and expects a model builder as an input argument. With a design principle that mirrors SwiftUI's Image modifiers, interacting with your model feels natural and intuitive. For instance, the `.resizable()` modifier scales your model to occupy the entire container space. 22 | 23 | ## 🔍 Technical Details 24 | * File Support: Currently, only the STL and OBJ file formats are supported. 25 | * Camera Details: An orthographic camera is calibrated to the view size and coordinated like its SwiftUI counterpart. 26 | * Mesh Information: The library leans on ModelIO for both model loading and generation. 27 | * Rendering: Rendering is based on Metal with MetalKit. 28 | 29 | ## 📜 Planned features 30 | The material system is inspired by spline.design, so the goal is to make any visual appearance reproducible by the library. 31 | 32 | | Materials | Status | Comment | 33 | |------------|-------------------|----------------------------------------------------------------------------| 34 | | Color | ✅ done | | 35 | | Matcap | ✅ done | | 36 | | Fresnel | 🟡 partially done | `Factor` is missing. The result doesn't match precisely with spline.design | 37 | | Texture | 🟡 partially done | Currently only samples based on UV | 38 | | Light | ⚙ in progress | | 39 | | Normal | todo | | 40 | | Depth | todo | | 41 | | Gradient | todo | | 42 | | Noise | todo | | 43 | | Rainbow | todo | | 44 | | Outline | todo | | 45 | | Glass | todo | | 46 | | Pattern | todo | | 47 | 48 | The library provides some basic primitive shapes, but making the shape generation more advanced is not planned so far. 49 | The main focus will be on rendering models from files. 50 | 51 | | Meshes | Status | 52 | |------------|-------------------| 53 | | Sphere | ✅ done | 54 | | Cube | ✅ done | 55 | | Model | 🟡 partially done | 56 | | Plane | todo | 57 | | Cylinder | todo | 58 | | Cone | todo | 59 | | ... | todo | 60 | 61 | The most common post-processing effects are planned. The list is not full yet. 62 | 63 | | Post processing | Status | 64 | |-----------------|--------------| 65 | | Bloom | todo | 66 | | Aberration | todo | 67 | | ... | todo | 68 | 69 | 70 | ## 🚧 Plans for v0.3 - Provide convenient way to combine materials 71 | - [ ] ~Scene background customization.~ (Just use `.background` of the View) 72 | - [X] Modifiers: `offset`, `rotation`. 73 | - [X] Modifiers: `frame` and `padding`. 74 | - [X] Materials composition 75 | - [X] Add materials: `fresnel`, `texture` 76 | - [X] Blend modes: `multiply`, `screen`, `overlay` 77 | - [ ] Add `light` material 78 | - [ ] Animations support out of the box (Currently supported using Animatable modifier on the parent View) 79 | - [ ] Multiple Meshes support 80 | - [ ] Bloom effect support 81 | 82 | ## 🚧 Plans for v0.4 - Rework objects management 83 | - [ ] Split Mesh into separate types, like "Sphere(), Cube(), Model()" 84 | - [ ] Add ability to apply separate materials to submeshes 85 | - [ ] Add Scene type which can handle adding USD 86 | ```Swift 87 | CreamyView { 88 | Scene(name: "my_scene.usdz") { 89 | Object(name: "my object") { // Define materials for the object named "my object" 90 | ColorMaterial(color: .white) 91 | LightMaterial(type: .physical) { 92 | DirectionalLight(direction: .init(x: 1.0, y: 1.0, z: 0.0)) 93 | } 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ## 🚧 Plans for v0.5 - Advanced animations 100 | - [ ] Material transition animations 101 | 102 | ## 🤔 Features under Consideration 103 | * Clonner, which repeats Meshes. Example: 104 | ```Swift 105 | Clonner(.grid(.init(x: 10, y: 10, z: 10)), spacing: 16.0) { 106 | Mesh(source: .sphere) 107 | .resizable() 108 | .frame(width: 50.0, height: 50.0) 109 | } 110 | ``` 111 | 112 | * Animated material transitions. The interface is under consideration. One option is: 113 | ```Swift 114 | Mesh(source: .sphere) { 115 | Transition(.fade, value: isSwitched) { value in 116 | if value { 117 | ColorMaterial(color: .red) 118 | } else { 119 | ColorMaterial(black: .black) 120 | } 121 | } 122 | } 123 | ``` 124 | -------------------------------------------------------------------------------- /Sources/Creamy3D/ColorRGB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorRGB.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ColorRGB { 11 | public let r: Double 12 | public let g: Double 13 | public let b: Double 14 | 15 | public init(r: Double, g: Double, b: Double) { 16 | self.r = r 17 | self.g = g 18 | self.b = b 19 | } 20 | 21 | public init(grayscale: Double) { 22 | self.r = grayscale 23 | self.g = grayscale 24 | self.b = grayscale 25 | } 26 | 27 | public func linearToSRGB() -> Self { 28 | .init( 29 | r: Self.linearToSRGB(r), 30 | g: Self.linearToSRGB(g), 31 | b: Self.linearToSRGB(b) 32 | ) 33 | } 34 | 35 | public func SRGBToLinear() -> Self { 36 | .init( 37 | r: Self.sRGBToLinear(r), 38 | g: Self.sRGBToLinear(g), 39 | b: Self.sRGBToLinear(b) 40 | ) 41 | } 42 | 43 | // MARK: - Factories 44 | 45 | @inlinable 46 | public static var black: Self { 47 | .init(r: 0.0, g: 0.0, b: 0.0) 48 | } 49 | 50 | @inlinable 51 | public static var white: Self { 52 | .init(r: 1.0, g: 1.0, b: 1.0) 53 | } 54 | 55 | @inlinable 56 | public static var red: Self { 57 | .init(r: 1.0, g: 0.0, b: 0.0) 58 | } 59 | 60 | @inlinable 61 | public static var green: Self { 62 | .init(r: 0.0, g: 1.0, b: 0.0) 63 | } 64 | 65 | @inlinable 66 | public static var blue: Self { 67 | .init(r: 0.0, g: 0.0, b: 1.0) 68 | } 69 | 70 | // MARK: - Internal 71 | 72 | @inlinable 73 | var simd: SIMD3 { 74 | .init(Float(r), Float(g), Float(b)) 75 | } 76 | 77 | // MARK: - Utils 78 | 79 | public static func linearToSRGB(_ linearValue: Double) -> Double { 80 | if linearValue <= 0.0031308 { 81 | return 12.92 * linearValue 82 | } else { 83 | return 1.055 * pow(linearValue, 1.0/2.4) - 0.055 84 | } 85 | } 86 | 87 | public static func sRGBToLinear(_ sRGBValue: Double) -> Double { 88 | if sRGBValue <= 0.04045 { 89 | return sRGBValue / 12.92 90 | } else { 91 | return pow((sRGBValue + 0.055) / 1.055, 2.4) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/Creamy3D/CreamyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreamyView.swift 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import simd 10 | 11 | public struct CameraPlacement { 12 | public let position: SIMD3 13 | public let target: SIMD3 14 | 15 | public static func from(_ position: SIMD3, lookAt: SIMD3) -> Self { 16 | .init(position: position, target: lookAt) 17 | } 18 | 19 | public func movingTo(_ newPosition: SIMD3) -> Self { 20 | .init(position: newPosition, target: target) 21 | } 22 | } 23 | 24 | public struct CreamyView: View { 25 | let cameraPlacement: CameraPlacement 26 | let objects: [any Object] 27 | 28 | public init( 29 | cameraPlacement: CameraPlacement = .from(.init(x: 0.0, y: 0.0, z: -1000.0), lookAt: .zero), 30 | @ObjectBuilder makeScene: () -> [any Object] 31 | ) { 32 | self.cameraPlacement = cameraPlacement 33 | self.objects = makeScene() 34 | } 35 | 36 | public var body: some View { 37 | GeometryReader { proxy in 38 | MetalView( 39 | projection: .init(width: proxy.size.width, 40 | height: proxy.size.height, 41 | nearZ: 0.001, 42 | farZ: .greatestFiniteMagnitude), 43 | camera: .init(position: cameraPlacement.position, 44 | target: cameraPlacement.target, 45 | up: .init(x: 0.0, y: 1.0, z: 0.0)), 46 | objects: objects 47 | ) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 17.08.24. 6 | // 7 | 8 | import OSLog 9 | 10 | @MainActor 11 | extension Logger { 12 | private static var subsystem = Bundle.main.bundleIdentifier! 13 | 14 | static let sceneLoading = Logger(subsystem: subsystem, category: "loading") 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Materials/ColorMaterial.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorMaterial.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ColorMaterial: MeshMaterial { 11 | let color: ColorRGB 12 | 13 | public init(color: ColorRGB) { 14 | self.color = color 15 | } 16 | 17 | public func makeFunction() -> MaterialFunction { 18 | ColorMaterialFunction(color: color.SRGBToLinear().simd) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Materials/FresnelMaterial.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 01.10.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct FresnelMaterial: MeshMaterial { 11 | let color: ColorRGB 12 | let scale: CGFloat 13 | let intensity: CGFloat 14 | let bias: CGFloat 15 | 16 | public init( 17 | color: ColorRGB, 18 | bias: CGFloat = 0.0, 19 | scale: CGFloat = 1.0, 20 | intensity: CGFloat 21 | ) { 22 | self.color = color 23 | self.scale = scale 24 | self.intensity = intensity 25 | self.bias = bias 26 | } 27 | 28 | public func makeFunction() -> MaterialFunction { 29 | FresnelMaterialFunction( 30 | color: color.SRGBToLinear().simd, 31 | bias: ColorRGB.sRGBToLinear(bias), 32 | scale: scale, 33 | intensity: intensity 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Materials/MatcapMaterial.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatcapMaterial.swift 3 | // MilkWaves 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MatcapMaterial: MeshMaterial { 11 | let name: String 12 | 13 | public init(name: String) { 14 | self.name = name 15 | } 16 | 17 | public func makeFunction() -> MaterialFunction { 18 | MatcapMaterialFunction(textureName: name) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Materials/Material+Blend.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Material+Blend.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 01.10.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum MeshMaterialBlend { 11 | case normal, multiply, screen, overlay 12 | } 13 | 14 | public extension MeshMaterial { 15 | 16 | func blend(_ mode: MeshMaterialBlend = .normal, _ alpha: CGFloat) -> some MeshMaterial { 17 | BlendedMaterial(base: self, blend: mode, alpha: alpha) 18 | } 19 | } 20 | 21 | struct BlendedMaterial: MeshMaterial { 22 | let base: Base 23 | let blend: MeshMaterialBlend 24 | let alpha: CGFloat 25 | 26 | func makeFunction() -> MaterialFunction { 27 | base.makeFunction() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Materials/Material.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Material.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol MeshMaterial { 11 | var alpha: CGFloat { get } 12 | var blend: MeshMaterialBlend { get } 13 | 14 | func makeFunction() -> MaterialFunction 15 | } 16 | 17 | public extension MeshMaterial { 18 | 19 | var alpha: CGFloat { 20 | 1.0 21 | } 22 | 23 | var blend: MeshMaterialBlend { 24 | .normal 25 | } 26 | } 27 | 28 | @resultBuilder 29 | public enum MeshMaterialBuilder { 30 | 31 | public func buildBlock() -> [any MeshMaterial] { 32 | [ColorMaterial(color: .black)] 33 | } 34 | 35 | public static func buildBlock(_ components: any MeshMaterial...) -> [any MeshMaterial] { 36 | components 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Materials/TextureMaterial.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 05.10.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TextureMaterial: MeshMaterial { 11 | let name: String 12 | 13 | public init(name: String) { 14 | self.name = name 15 | } 16 | 17 | public func makeFunction() -> MaterialFunction { 18 | TextureMaterialFunction(textureName: name) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Creamy3D/MetalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalView.swift 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 04.03.23. 6 | // 7 | 8 | import MetalKit 9 | import SwiftUI 10 | 11 | struct MetalView: UIViewRepresentable { 12 | let projection: Projection 13 | let camera: Camera 14 | let objects: [any Object] 15 | 16 | @StateObject 17 | var renderer = Renderer() 18 | 19 | func makeUIView(context: Context) -> MTKView { 20 | let view = MTKView() 21 | renderer.setup(view: view) 22 | renderer.update(camera: camera, projection: projection) 23 | renderer.update(objects: objects, projection: projection, view: view) 24 | return view 25 | } 26 | 27 | func updateUIView(_ uiView: MTKView, context: Context) { 28 | renderer.update(camera: camera, projection: projection) 29 | renderer.update(objects: objects, projection: projection, view: uiView) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Objects/EmptyObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyObject.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct EmptyObject: Object { 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Objects/Mesh.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mesh.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct Mesh: Object { 11 | 12 | public enum Source { 13 | case sphere 14 | case cube 15 | case obj(String) 16 | case stl(String) 17 | } 18 | 19 | internal struct Options { 20 | var isResizable: Bool 21 | var aspectRatio: (CGFloat?, ContentMode)? 22 | var offset: CGSize 23 | var rotation: (angle: Angle, axis: SIMD3) 24 | var frame: (width: CGFloat?, height: CGFloat?, depth: CGFloat?)? 25 | var insets: EdgeInsets 26 | var shouldGenerateNormals: Bool 27 | } 28 | 29 | let id: String 30 | let source: Source 31 | let materials: [any MeshMaterial] 32 | let options: Options 33 | 34 | public init( 35 | id: String, 36 | source: Source, 37 | @MeshMaterialBuilder makeMaterials: () -> [any MeshMaterial] 38 | ) { 39 | self.init( 40 | id: id, 41 | source: source, 42 | materials: makeMaterials(), 43 | options: .init( 44 | isResizable: false, 45 | aspectRatio: nil, 46 | offset: .zero, 47 | rotation: (.zero, .zero), 48 | frame: nil, 49 | insets: .init(), 50 | shouldGenerateNormals: false 51 | ) 52 | ) 53 | } 54 | 55 | internal init( 56 | id: String, 57 | source: Source, 58 | materials: [any MeshMaterial], 59 | options: Options 60 | ) { 61 | self.id = id 62 | self.source = source 63 | self.options = options 64 | self.materials = materials 65 | } 66 | } 67 | 68 | public extension Mesh { 69 | 70 | func resizable() -> Self { 71 | var options = self.options 72 | options.isResizable = true 73 | return .init( 74 | id: id, 75 | source: source, 76 | materials: materials, 77 | options: options 78 | ) 79 | } 80 | 81 | func aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode) -> Self { 82 | var options = self.options 83 | options.aspectRatio = (aspectRatio, contentMode) 84 | return .init( 85 | id: id, 86 | source: source, 87 | materials: materials, 88 | options: options 89 | ) 90 | } 91 | 92 | @inlinable 93 | func scaledToFill() -> Self { 94 | aspectRatio(contentMode: .fill) 95 | } 96 | 97 | @inlinable 98 | func scaledToFit() -> Self { 99 | aspectRatio(contentMode: .fit) 100 | } 101 | 102 | func generateNormals(_ shouldGenerate: Bool = true) -> Self { 103 | var options = self.options 104 | options.shouldGenerateNormals = shouldGenerate 105 | return .init( 106 | id: id, 107 | source: source, 108 | materials: materials, 109 | options: options 110 | ) 111 | } 112 | } 113 | 114 | public extension Mesh { 115 | 116 | func offset(_ offset: CGSize) -> Self { 117 | var options = self.options 118 | options.offset = offset 119 | return .init( 120 | id: id, 121 | source: source, 122 | materials: materials, 123 | options: options 124 | ) 125 | } 126 | 127 | @inlinable 128 | func offset(x: CGFloat = 0, y: CGFloat = 0) -> Mesh { 129 | offset(.init(width: x, height: y)) 130 | } 131 | } 132 | 133 | public extension Mesh { 134 | 135 | func rotation( 136 | _ angle: Angle, 137 | axis: (x: CGFloat, y: CGFloat, z: CGFloat) 138 | ) -> Mesh { 139 | var options = self.options 140 | options.rotation = (angle, .init(axis.x, axis.y, axis.z)) 141 | return .init( 142 | id: id, 143 | source: source, 144 | materials: materials, 145 | options: options 146 | ) 147 | } 148 | } 149 | 150 | public extension Mesh { 151 | 152 | func frame( 153 | width: CGFloat? = nil, 154 | height: CGFloat? = nil, 155 | depth: CGFloat? = nil 156 | ) -> Mesh { 157 | var options = self.options 158 | options.frame = (width, height, depth) 159 | return .init( 160 | id: id, 161 | source: source, 162 | materials: materials, 163 | options: options 164 | ) 165 | } 166 | 167 | @available(*, deprecated, message: "Please pass one or more parameters.") 168 | func frame() -> Mesh { 169 | self 170 | } 171 | } 172 | 173 | public extension Mesh { 174 | 175 | func padding(_ insets: EdgeInsets) -> Mesh { 176 | var options = self.options 177 | options.insets = insets 178 | return .init( 179 | id: id, 180 | source: source, 181 | materials: materials, 182 | options: options 183 | ) 184 | } 185 | 186 | func padding(_ edges: Edge.Set = .all, _ length: CGFloat = 16.0) -> Mesh { 187 | padding(insets(edges: edges, length: length)) 188 | } 189 | 190 | @inlinable 191 | func padding(_ length: CGFloat = 16.0) -> Mesh { 192 | padding(.all, length) 193 | } 194 | 195 | private func insets(edges: Edge.Set, length: CGFloat) -> EdgeInsets { 196 | .init( 197 | top: edges.contains(.top) ? length : 0.0, 198 | leading: edges.contains(.leading) ? length : 0.0, 199 | bottom: edges.contains(.bottom) ? length : 0.0, 200 | trailing: edges.contains(.trailing) ? length : 0.0 201 | ) 202 | } 203 | } 204 | 205 | extension Mesh { 206 | 207 | func loader() -> any MeshLoader { 208 | switch source { 209 | case .sphere: 210 | return SphereMeshLoader( 211 | radii: .one, 212 | radialSegments: 100, 213 | verticalSegments: 100 214 | ) 215 | case .cube: 216 | return CubeMeshLoader( 217 | dimensions: .one, 218 | segments: .one 219 | ) 220 | case let .obj(name): 221 | return ModelMeshLoader( 222 | name: name, 223 | ext: "obj", 224 | shouldGenerateNormals: options.shouldGenerateNormals 225 | ) 226 | case let .stl(name): 227 | return ModelMeshLoader( 228 | name: name, 229 | ext: "stl", 230 | shouldGenerateNormals: options.shouldGenerateNormals 231 | ) 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Objects/Object.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Object.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Object { 11 | } 12 | 13 | @resultBuilder 14 | public enum ObjectBuilder { 15 | 16 | public static func buildBlock(_ objects: Object...) -> [any Object] { 17 | objects 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Renderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Renderer.swift 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 04.03.23. 6 | // 7 | 8 | import MetalKit 9 | import SwiftUI 10 | import OSLog 11 | 12 | @MainActor 13 | final class Renderer: NSObject, ObservableObject { 14 | 15 | struct Config { 16 | let sampleCount: Int 17 | let colorPixelFormat: MTLPixelFormat 18 | let depthPixelFormat: MTLPixelFormat 19 | } 20 | 21 | private static let buffersCount = 3 22 | private let config: Config 23 | 24 | // MARK: Metal 25 | public let device: MTLDevice 26 | public let library: MTLLibrary 27 | public let commandQueue: MTLCommandQueue 28 | 29 | // MARK: Resources 30 | private let meshAllocator: MTKMeshBufferAllocator 31 | private let textureLoader: MTKTextureLoader 32 | 33 | // MARK: Syncronization 34 | private let semaphore = DispatchSemaphore(value: Renderer.buffersCount) 35 | private var currentBuffer = 0 36 | private var frameIndex = 0 37 | 38 | // MARK: Scene 39 | private let startTime: TimeInterval 40 | 41 | private var viewMatrix = float4x4() 42 | private var projectionMatrix = float4x4() 43 | 44 | // MARK: - Mesh 45 | private var meshes = [String: MeshNode]() 46 | 47 | // MARK: - Init 48 | 49 | override init() { 50 | guard let device = MTLCreateSystemDefaultDevice() else { 51 | fatalError("Failed to create device") 52 | } 53 | guard let library = try? device.makeDefaultLibrary(bundle: Bundle.module) else { 54 | fatalError("Failed to create a library") 55 | } 56 | guard let commandQueue = device.makeCommandQueue() else { 57 | fatalError("Failed to create a command queue") 58 | } 59 | 60 | self.config = Self.makeConfig(device: device) 61 | self.device = device 62 | self.library = library 63 | self.commandQueue = commandQueue 64 | self.meshAllocator = MTKMeshBufferAllocator(device: device) 65 | self.textureLoader = MTKTextureLoader(device: device) 66 | 67 | self.startTime = Date().timeIntervalSince1970 68 | 69 | super.init() 70 | } 71 | 72 | func setup(view: MTKView) { 73 | view.delegate = self 74 | view.device = device 75 | view.colorPixelFormat = config.colorPixelFormat 76 | view.framebufferOnly = true 77 | view.clearDepth = 1.0 78 | view.clearColor = .init(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) 79 | view.depthStencilPixelFormat = config.depthPixelFormat 80 | view.backgroundColor = .clear 81 | view.sampleCount = config.sampleCount 82 | } 83 | 84 | func update(camera: Camera, projection: Projection) { 85 | self.viewMatrix = camera.makeMatrix() 86 | self.projectionMatrix = projection.makeMatrix() 87 | } 88 | 89 | func update(objects: [any Object], projection: Projection, view: MTKView) { 90 | let meshes = objects.compactMap { $0 as? Mesh } 91 | // Remove meshes that are not in the list anymore 92 | let allIDs = meshes.map { $0.id } 93 | self.meshes = self.meshes.filter { allIDs.contains($0.key) } 94 | // Update the rest 95 | meshes.forEach { mesh in 96 | self.update(mesh: mesh, projection: projection) 97 | } 98 | view.isPaused = false 99 | } 100 | 101 | // MARK: - Utils 102 | 103 | private func update(mesh: Mesh, projection: Projection) { 104 | if let node = meshes[mesh.id] { 105 | node.update( 106 | mesh: mesh, 107 | projection: projection 108 | ) 109 | Logger.sceneLoading.debug("Updated node: \(mesh.id)") 110 | } else { 111 | do { 112 | let node = MeshNode() 113 | try node.setup( 114 | mesh: mesh, 115 | config: config, 116 | allocator: meshAllocator, 117 | textureLoader: textureLoader, 118 | device: device, 119 | library: library 120 | ) 121 | node.update( 122 | mesh: mesh, 123 | projection: projection 124 | ) 125 | meshes[mesh.id] = node 126 | Logger.sceneLoading.debug("Added node: \(mesh.id)") 127 | } catch { 128 | Logger.sceneLoading.error("Failed to add mesh(\(mesh.id): \(error)") 129 | } 130 | } 131 | } 132 | 133 | private static func makeConfig(device: some MTLDevice) -> Config { 134 | let samples = [8, 4, 2] 135 | let sampleCount = samples.first { device.supportsTextureSampleCount($0) } ?? 1 136 | 137 | return .init( 138 | sampleCount: sampleCount, 139 | colorPixelFormat: .bgra8Unorm_srgb, 140 | depthPixelFormat: .depth32Float 141 | ) 142 | } 143 | } 144 | 145 | extension Renderer: MTKViewDelegate { 146 | 147 | public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 148 | Logger.sceneLoading.info("Scene resized to (\(size.width), \(size.height))") 149 | } 150 | 151 | public func draw(in view: MTKView) { 152 | semaphore.wait() 153 | 154 | autoreleasepool { 155 | executePasses(view: view) { 156 | self.semaphore.signal() 157 | } 158 | } 159 | } 160 | 161 | private func executePasses(view: MTKView, completion: @Sendable @escaping () -> Void) { 162 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { 163 | return 164 | } 165 | guard let drawable = view.currentDrawable else { 166 | return 167 | } 168 | guard let descriptor = view.currentRenderPassDescriptor else { 169 | return 170 | } 171 | 172 | // Encode all meshes 173 | guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { 174 | return 175 | } 176 | encoder.setCullMode(.back) 177 | encoder.setFrontFacing(.counterClockwise) 178 | 179 | meshes.values.forEach { mesh in 180 | mesh.render(encoder: encoder, 181 | viewProjectionMatrix: projectionMatrix * viewMatrix, 182 | viewMatrix: viewMatrix, 183 | deltaTime: startTime - Date().timeIntervalSince1970) 184 | } 185 | encoder.endEncoding() 186 | 187 | commandBuffer.present(drawable) 188 | 189 | // Finalize 190 | commandBuffer.addCompletedHandler { @Sendable _ in 191 | completion() 192 | } 193 | commandBuffer.commit() 194 | 195 | currentBuffer = (currentBuffer + 1) % Self.buffersCount 196 | frameIndex = (frameIndex + 1) % 64 197 | 198 | view.isPaused = true 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/Camera.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import simd 9 | 10 | struct Camera { 11 | let position: simd_float3 12 | let target: simd_float3 13 | let up: simd_float3 14 | 15 | func makeMatrix() -> float4x4 { 16 | let zAxis = normalize(position - target) 17 | let xAxis = normalize(cross(up, zAxis)) 18 | let yAxis = cross(zAxis, xAxis) 19 | 20 | let matrix = simd_float4x4( 21 | simd_float4(xAxis.x, yAxis.x, zAxis.x, 0), 22 | simd_float4(xAxis.y, yAxis.y, zAxis.y, 0), 23 | simd_float4(xAxis.z, yAxis.z, zAxis.z, 0), 24 | simd_float4(-dot(xAxis, position), -dot(yAxis, position), -dot(zAxis, position), 1) 25 | ) 26 | 27 | return matrix 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MaterialFunctions/ColorMaterialFunction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorMaterialFunction.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 30.09.23. 6 | // 7 | 8 | import Metal 9 | import MetalKit 10 | 11 | private struct ColorMaterialArguments { 12 | var color: SIMD3 13 | } 14 | 15 | final class ColorMaterialFunction: MaterialFunction { 16 | 17 | let color: SIMD3 18 | 19 | init(color: SIMD3) { 20 | self.color = color 21 | } 22 | 23 | var functionName: String { 24 | "color_material" 25 | } 26 | 27 | func loadResources(textureLoader: MTKTextureLoader) throws { 28 | } 29 | 30 | var resourcesSize: Int { 31 | MemoryLayout.stride 32 | } 33 | 34 | func assignResources(pointer: UnsafeMutableRawPointer) { 35 | let binded = pointer.bindMemory(to: ColorMaterialArguments.self, capacity: 1) 36 | binded.pointee.color = color 37 | } 38 | 39 | func useResources(encoder: MTLRenderCommandEncoder) { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MaterialFunctions/FresnelMaterialFunction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 01.10.23. 6 | // 7 | 8 | import Metal 9 | import MetalKit 10 | 11 | private struct FresnelMaterialArguments { 12 | var color: SIMD3 13 | var intensity: Float 14 | var scale: Float 15 | var bias: Float 16 | } 17 | 18 | final class FresnelMaterialFunction: MaterialFunction { 19 | let color: SIMD3 20 | let intensity: CGFloat 21 | let scale: CGFloat 22 | let bias: CGFloat 23 | 24 | init(color: SIMD3, bias: CGFloat, scale: CGFloat, intensity: CGFloat) { 25 | self.color = color 26 | self.scale = scale 27 | self.intensity = intensity 28 | self.bias = bias 29 | } 30 | 31 | var functionName: String { 32 | "fresnel_material" 33 | } 34 | 35 | func loadResources(textureLoader: MTKTextureLoader) throws { 36 | } 37 | 38 | var resourcesSize: Int { 39 | MemoryLayout.stride 40 | } 41 | 42 | func assignResources(pointer: UnsafeMutableRawPointer) { 43 | let binded = pointer.bindMemory(to: FresnelMaterialArguments.self, capacity: 1) 44 | binded.pointee.intensity = Float(intensity) 45 | binded.pointee.color = color 46 | binded.pointee.scale = Float(scale) 47 | binded.pointee.bias = Float(bias) 48 | } 49 | 50 | func useResources(encoder: MTLRenderCommandEncoder) { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MaterialFunctions/MatcapMaterialFunction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatcapMaterialFunction.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 30.09.23. 6 | // 7 | 8 | import Metal 9 | import MetalKit 10 | 11 | private struct MatcapMaterialArguments { 12 | var textureID: MTLResourceID 13 | } 14 | 15 | final class MatcapMaterialFunction: MaterialFunction { 16 | let textureName: String 17 | 18 | init(textureName: String) { 19 | self.textureName = textureName 20 | } 21 | 22 | var texture: MTLTexture! 23 | 24 | var functionName: String { 25 | "matcap_material" 26 | } 27 | 28 | func loadResources(textureLoader: MTKTextureLoader) throws { 29 | do { 30 | texture = try textureLoader.newTexture( 31 | name: textureName, 32 | scaleFactor: 1.0, 33 | bundle: nil, 34 | options: [.SRGB: true] 35 | ) 36 | } catch { 37 | throw MaterialFunctionError.failedToLoadResource 38 | } 39 | } 40 | 41 | var resourcesSize: Int { 42 | MemoryLayout.stride 43 | } 44 | 45 | func assignResources(pointer: UnsafeMutableRawPointer) { 46 | let binded = pointer.bindMemory(to: MatcapMaterialArguments.self, capacity: 1) 47 | binded.pointee.textureID = texture.gpuResourceID 48 | } 49 | 50 | func useResources(encoder: MTLRenderCommandEncoder) { 51 | encoder.useResource(texture, usage: .read, stages: .fragment) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MaterialFunctions/MaterialFunction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaterialFunction.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 30.09.23. 6 | // 7 | 8 | import Metal 9 | import MetalKit 10 | 11 | public enum MaterialFunctionError: Error { 12 | case failedToCreateFunction 13 | case failedToLoadResource 14 | } 15 | 16 | public protocol MaterialFunction { 17 | 18 | 19 | func loadResources(textureLoader: MTKTextureLoader) throws 20 | 21 | var resourcesSize: Int { get } 22 | func assignResources(pointer: UnsafeMutableRawPointer) 23 | func useResources(encoder: MTLRenderCommandEncoder) 24 | 25 | var functionName: String { get } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MaterialFunctions/TextureMaterialFunction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 05.10.23. 6 | // 7 | 8 | import Metal 9 | import MetalKit 10 | 11 | private struct TextureMaterialArguments { 12 | var textureID: MTLResourceID 13 | } 14 | 15 | final class TextureMaterialFunction: MaterialFunction { 16 | let textureName: String 17 | 18 | init(textureName: String) { 19 | self.textureName = textureName 20 | } 21 | 22 | var texture: MTLTexture! 23 | 24 | var functionName: String { 25 | "texture_material" 26 | } 27 | 28 | func loadResources(textureLoader: MTKTextureLoader) throws { 29 | do { 30 | texture = try textureLoader.newTexture( 31 | name: textureName, 32 | scaleFactor: 1.0, 33 | bundle: nil, 34 | options: [.SRGB: true] 35 | ) 36 | } catch { 37 | throw MaterialFunctionError.failedToLoadResource 38 | } 39 | } 40 | 41 | var resourcesSize: Int { 42 | MemoryLayout.stride 43 | } 44 | 45 | func assignResources(pointer: UnsafeMutableRawPointer) { 46 | let binded = pointer.bindMemory(to: TextureMaterialArguments.self, capacity: 1) 47 | binded.pointee.textureID = texture.gpuResourceID 48 | } 49 | 50 | func useResources(encoder: MTLRenderCommandEncoder) { 51 | encoder.useResource(texture, usage: .read, stages: .fragment) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MaterialState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import MetalKit 9 | import simd 10 | 11 | enum MaterialError: Error { 12 | case failedToLoadTexture 13 | } 14 | 15 | struct ShaderMaterial { 16 | var functionIndex: Int32 17 | var resourceIndex: Int32 18 | var blend: Int32 19 | var alpha: Float 20 | } 21 | 22 | final class MaterialState { 23 | 24 | private var pipelineState: MTLRenderPipelineState! 25 | private var depthPipelineState: MTLDepthStencilState! 26 | 27 | private var materialFunctions = [any MaterialFunction]() 28 | private var resourcesBuffer: MTLBuffer! 29 | private var materialsBuffer: MTLBuffer! 30 | private var mtlFunctions = [MTLFunction]() 31 | 32 | func setup( 33 | materials: [any MeshMaterial], 34 | config: Renderer.Config, 35 | device: MTLDevice, 36 | library: MTLLibrary, 37 | textureLoader: MTKTextureLoader 38 | ) throws { 39 | materialFunctions = try materials.map { material in 40 | let function = material.makeFunction() 41 | 42 | if !mtlFunctions.contains(where: { $0.name == function.functionName }) { 43 | let mtlFunction = library.makeFunction(name: function.functionName)! 44 | mtlFunctions.append(mtlFunction) 45 | } 46 | 47 | try function.loadResources(textureLoader: textureLoader) 48 | return function 49 | } 50 | 51 | var (stride, buffer) = makeResourcesBuffer(device: device, functions: materialFunctions) 52 | var materialsCount = Int32(materialFunctions.count) 53 | 54 | resourcesBuffer = buffer 55 | materialsBuffer = makeMaterialsBuffer( 56 | device: device, 57 | materials: zip(materials, materialFunctions).map { ($0, $1) } 58 | ) 59 | 60 | let constants = MTLFunctionConstantValues() 61 | constants.setConstantValue(&stride, type: .uint, index: 0) 62 | constants.setConstantValue(&materialsCount, type: .int, index: 1) 63 | 64 | let vertexFunction = library.makeFunction(name: "vertex_common") 65 | let fragmentFunction = try! library.makeFunction(name: "fragment_common", constantValues: constants) 66 | 67 | let pipelineDescriptor = MTLRenderPipelineDescriptor() 68 | pipelineDescriptor.vertexFunction = vertexFunction 69 | pipelineDescriptor.fragmentFunction = fragmentFunction 70 | pipelineDescriptor.vertexDescriptor = makeVertexDescriptor() 71 | pipelineDescriptor.colorAttachments[0].pixelFormat = config.colorPixelFormat 72 | pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true 73 | pipelineDescriptor.rasterSampleCount = config.sampleCount 74 | 75 | pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add 76 | pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add 77 | 78 | pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha 79 | pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha 80 | 81 | pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha 82 | pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha 83 | pipelineDescriptor.depthAttachmentPixelFormat = config.depthPixelFormat 84 | 85 | let linkedFunctions = MTLLinkedFunctions() 86 | linkedFunctions.functions = mtlFunctions 87 | linkedFunctions.groups = [ 88 | "material": mtlFunctions 89 | ] 90 | 91 | pipelineDescriptor.fragmentLinkedFunctions = linkedFunctions 92 | 93 | do { 94 | self.pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) 95 | } catch { 96 | fatalError("Failed to create present pass \(error)") 97 | } 98 | 99 | let depthDescriptor = MTLDepthStencilDescriptor() 100 | depthDescriptor.depthCompareFunction = .less 101 | depthDescriptor.isDepthWriteEnabled = true 102 | 103 | self.depthPipelineState = device.makeDepthStencilState(descriptor: depthDescriptor)! 104 | } 105 | 106 | func activate(encoder: MTLRenderCommandEncoder) { 107 | encoder.setDepthStencilState(depthPipelineState) 108 | encoder.setRenderPipelineState(pipelineState) 109 | 110 | // Setup functions for every material 111 | let materialsTableDescriptor = MTLVisibleFunctionTableDescriptor() 112 | materialsTableDescriptor.functionCount = mtlFunctions.count 113 | 114 | let materialsTable = pipelineState.makeVisibleFunctionTable(descriptor: materialsTableDescriptor, stage: .fragment)! 115 | for (i, function) in mtlFunctions.enumerated() { 116 | let functionHandle = pipelineState.functionHandle( 117 | function: function, 118 | stage: .fragment 119 | )! 120 | materialsTable.setFunction(functionHandle, index: i) 121 | } 122 | 123 | encoder.setFragmentVisibleFunctionTable(materialsTable, bufferIndex: 0) 124 | 125 | 126 | encoder.setFragmentBuffer(resourcesBuffer, offset: 0, index: 1) 127 | encoder.setFragmentBuffer(materialsBuffer, offset: 0, index: 2) 128 | 129 | for materialFunction in materialFunctions { 130 | materialFunction.useResources(encoder: encoder) 131 | } 132 | } 133 | 134 | // MARK: - Utils 135 | 136 | private func makeVertexDescriptor() -> MTLVertexDescriptor { 137 | // Create the vertex descriptor 138 | let vertexDescriptor = MTLVertexDescriptor() 139 | 140 | // Position attribute 141 | vertexDescriptor.attributes[0].format = .float4 142 | vertexDescriptor.attributes[0].offset = 0 143 | vertexDescriptor.attributes[0].bufferIndex = 0 144 | 145 | // Normal attribute 146 | vertexDescriptor.attributes[1].format = .float3 147 | vertexDescriptor.attributes[1].offset = MemoryLayout>.stride 148 | vertexDescriptor.attributes[1].bufferIndex = 0 149 | 150 | // Tangent attribute 151 | vertexDescriptor.attributes[2].format = .float3 152 | vertexDescriptor.attributes[2].offset = MemoryLayout>.stride + MemoryLayout>.stride 153 | vertexDescriptor.attributes[2].bufferIndex = 0 154 | 155 | // UV attribute 156 | vertexDescriptor.attributes[3].format = .float2 157 | vertexDescriptor.attributes[3].offset = MemoryLayout>.stride + MemoryLayout>.stride * 2 158 | vertexDescriptor.attributes[3].bufferIndex = 0 159 | 160 | // Create a single interleaved layout 161 | vertexDescriptor.layouts[0].stride = MemoryLayout>.stride + MemoryLayout>.stride * 2 + MemoryLayout>.stride 162 | vertexDescriptor.layouts[0].stepRate = 1 163 | vertexDescriptor.layouts[0].stepFunction = .perVertex 164 | return vertexDescriptor 165 | } 166 | 167 | private func makeResourcesBuffer(device: MTLDevice, functions: [any MaterialFunction]) -> (stride: UInt32, buffer: MTLBuffer) { 168 | var resourcesStride = 0 169 | for function in functions { 170 | if function.resourcesSize > resourcesStride { 171 | resourcesStride = function.resourcesSize 172 | } 173 | } 174 | 175 | let buffer = device.makeBuffer( 176 | length: resourcesStride * functions.count, 177 | options: .storageModeShared 178 | )! 179 | 180 | let pointer = buffer.contents() 181 | for (i, function) in functions.enumerated() { 182 | let advancedPointer = pointer.advanced(by: resourcesStride * i) 183 | function.assignResources(pointer: advancedPointer) 184 | } 185 | 186 | return (UInt32(resourcesStride), buffer) 187 | } 188 | 189 | private func makeMaterialsBuffer( 190 | device: MTLDevice, 191 | materials: [(material: any MeshMaterial, function: any MaterialFunction)] 192 | ) -> MTLBuffer { 193 | let buffer = device.makeBuffer( 194 | length: MemoryLayout.stride * materials.count, 195 | options: .storageModeShared 196 | )! 197 | buffer.label = "Materials" 198 | let pointer = buffer.contents() 199 | for (i, material) in materials.enumerated() { 200 | guard let index = mtlFunctions.firstIndex(where: { $0.name == material.function.functionName }) else { 201 | continue 202 | } 203 | let binded = pointer.advanced(by: MemoryLayout.stride * i) 204 | .bindMemory(to: ShaderMaterial.self, capacity: 1) 205 | binded.pointee.functionIndex = Int32(index) 206 | binded.pointee.resourceIndex = Int32(i) 207 | binded.pointee.blend = Int32(material.material.blend.index) 208 | binded.pointee.alpha = Float(material.material.alpha) 209 | } 210 | return buffer 211 | } 212 | } 213 | 214 | extension MeshMaterialBlend { 215 | 216 | var index: Int { 217 | switch self { 218 | case .normal: 219 | return 0 220 | case .multiply: 221 | return 1 222 | case .screen: 223 | return 2 224 | case .overlay: 225 | return 3 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MeshLoader/CubeMeshLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import MetalKit 9 | import ModelIO 10 | 11 | struct CubeMeshLoader: MeshLoader { 12 | let dimensions: SIMD3 13 | let segments: SIMD3 14 | 15 | func load(device: MTLDevice, allocator: MTKMeshBufferAllocator) throws -> LoadedMesh { 16 | let mesh = MDLMesh.newBox( 17 | withDimensions: dimensions, 18 | segments: segments, 19 | geometryType: .triangles, 20 | inwardNormals: false, 21 | allocator: allocator 22 | ) 23 | mesh.vertexDescriptor = makeVertexDescriptor() 24 | 25 | do { 26 | return .init( 27 | size: mesh.boundingBox.maxBounds - mesh.boundingBox.minBounds, 28 | mesh: try MTKMesh(mesh: mesh, device: device) 29 | ) 30 | } catch { 31 | throw MeshLoadingError.loadingFailed 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MeshLoader/MeshLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshLoader.swift 3 | // MilkWaves 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import MetalKit 9 | import ModelIO 10 | 11 | enum MeshLoadingError: Error { 12 | case fileNotFount 13 | case incorrectStructure 14 | case loadingFailed 15 | } 16 | 17 | struct LoadedMesh { 18 | let size: SIMD3 19 | let mesh: MTKMesh 20 | } 21 | 22 | protocol MeshLoader { 23 | func load(device: MTLDevice, allocator: MTKMeshBufferAllocator) throws -> LoadedMesh 24 | 25 | func makeVertexDescriptor() -> MDLVertexDescriptor 26 | } 27 | 28 | extension MeshLoader { 29 | 30 | func makeVertexDescriptor() -> MDLVertexDescriptor { 31 | let mdlVertexDescriptor = MDLVertexDescriptor() 32 | 33 | // Position attribute 34 | mdlVertexDescriptor.attributes[0] = MDLVertexAttribute( 35 | name: MDLVertexAttributePosition, 36 | format: .float4, 37 | offset: 0, 38 | bufferIndex: 0 39 | ) 40 | 41 | // Normal attribute 42 | mdlVertexDescriptor.attributes[1] = MDLVertexAttribute( 43 | name: MDLVertexAttributeNormal, 44 | format: .float3, 45 | offset: MemoryLayout>.stride, 46 | bufferIndex: 0 47 | ) 48 | 49 | mdlVertexDescriptor.attributes[2] = MDLVertexAttribute( 50 | name: MDLVertexAttributeTangent, 51 | format: .float3, 52 | offset: MemoryLayout>.stride + MemoryLayout>.stride, 53 | bufferIndex: 0 54 | ) 55 | 56 | // UV attribute 57 | mdlVertexDescriptor.attributes[3] = MDLVertexAttribute( 58 | name: MDLVertexAttributeTextureCoordinate, 59 | format: .float2, 60 | offset: MemoryLayout>.stride + MemoryLayout>.stride * 2, 61 | bufferIndex: 0 62 | ) 63 | 64 | // Create a single interleaved buffer layout 65 | mdlVertexDescriptor.layouts[0] = MDLVertexBufferLayout( 66 | stride: MemoryLayout>.stride + MemoryLayout>.stride * 2 + MemoryLayout>.stride 67 | ) 68 | 69 | return mdlVertexDescriptor 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MeshLoader/ModelMeshLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelMeshLoader.swift 3 | // MilkWaves 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import MetalKit 9 | import ModelIO 10 | 11 | struct ModelMeshLoader: MeshLoader { 12 | let name: String 13 | let ext: String 14 | let shouldGenerateNormals: Bool 15 | 16 | func load(device: MTLDevice, allocator: MTKMeshBufferAllocator) throws -> LoadedMesh { 17 | guard let url = Bundle.main.url(forResource: name, withExtension: ext) else { 18 | throw MeshLoadingError.fileNotFount 19 | } 20 | let asset = MDLAsset( 21 | url: url, 22 | vertexDescriptor: nil, 23 | bufferAllocator: allocator 24 | ) 25 | guard let mesh = asset.object(at: 0) as? MDLMesh else { 26 | throw MeshLoadingError.incorrectStructure 27 | } 28 | mesh.vertexDescriptor = makeVertexDescriptor() 29 | 30 | if shouldGenerateNormals { 31 | mesh.addNormals(withAttributeNamed: MDLVertexAttributeNormal, creaseThreshold: 0.01) 32 | } 33 | 34 | mesh.addOrthTanBasis( 35 | forTextureCoordinateAttributeNamed: MDLVertexAttributeTextureCoordinate, 36 | normalAttributeNamed: MDLVertexAttributeNormal, 37 | tangentAttributeNamed: MDLVertexAttributeTangent 38 | ) 39 | 40 | do { 41 | return .init( 42 | size: mesh.boundingBox.maxBounds - mesh.boundingBox.minBounds, 43 | mesh: try MTKMesh(mesh: mesh, device: device) 44 | ) 45 | } catch { 46 | throw MeshLoadingError.loadingFailed 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MeshLoader/SphereMeshLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SphereMeshLoader.swift 3 | // MilkWaves 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import MetalKit 9 | import ModelIO 10 | 11 | struct SphereMeshLoader: MeshLoader { 12 | let radii: SIMD3 13 | let radialSegments: Int 14 | let verticalSegments: Int 15 | 16 | func load(device: MTLDevice, allocator: MTKMeshBufferAllocator) throws -> LoadedMesh { 17 | let mesh = MDLMesh.newEllipsoid( 18 | withRadii: radii, 19 | radialSegments: radialSegments, 20 | verticalSegments: verticalSegments, 21 | geometryType: .triangles, 22 | inwardNormals: false, 23 | hemisphere: false, 24 | allocator: allocator 25 | ) 26 | mesh.vertexDescriptor = makeVertexDescriptor() 27 | 28 | do { 29 | return .init( 30 | size: mesh.boundingBox.maxBounds - mesh.boundingBox.minBounds, 31 | mesh: try MTKMesh(mesh: mesh, device: device) 32 | ) 33 | } catch { 34 | throw MeshLoadingError.loadingFailed 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/MeshNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshNode.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import simd 9 | import MetalKit 10 | 11 | struct MeshUniforms { 12 | let mvp: float4x4 13 | let model: float4x4 14 | let view: float4x4 15 | let normalModel: float3x3 16 | let time: Float 17 | } 18 | 19 | final class MeshNode: Transformable { 20 | var position = simd_float3.zero 21 | var scale = simd_float3.one 22 | var rotation = simd_quatf() 23 | 24 | var mesh: LoadedMesh? 25 | var materialState: MaterialState? 26 | } 27 | 28 | extension MeshNode: Renderable { 29 | 30 | func setup( 31 | mesh: Mesh, 32 | config: Renderer.Config, 33 | allocator: MTKMeshBufferAllocator, 34 | textureLoader: MTKTextureLoader, 35 | device: MTLDevice, 36 | library: MTLLibrary 37 | ) throws { 38 | let loader = mesh.loader() 39 | self.mesh = try loader.load(device: device, allocator: allocator) 40 | 41 | let state = MaterialState() 42 | try state.setup( 43 | materials: mesh.materials, 44 | config: config, 45 | device: device, 46 | library: library, 47 | textureLoader: textureLoader 48 | ) 49 | self.materialState = state 50 | } 51 | 52 | func update( 53 | mesh: Mesh, 54 | projection: Projection 55 | ) { 56 | let options = mesh.options 57 | self.position = calculatePosition(options: options) 58 | self.rotation = .init( 59 | angle: Float(options.rotation.0.radians), 60 | axis: .init(options.rotation.1) 61 | ) 62 | self.scale = calculateScale(options: options, projection: projection) 63 | } 64 | 65 | func render( 66 | encoder: MTLRenderCommandEncoder, 67 | viewProjectionMatrix: float4x4, 68 | viewMatrix: float4x4, 69 | deltaTime: Double 70 | ) { 71 | guard let mesh = self.mesh?.mesh, let materialState else { 72 | return 73 | } 74 | var uniforms = MeshUniforms( 75 | mvp: viewProjectionMatrix * model, 76 | model: model, 77 | view: viewMatrix, 78 | normalModel: calculateNormalMatrix(model), 79 | time: Float(deltaTime) 80 | ) 81 | 82 | // Set the pipeline state 83 | materialState.activate(encoder: encoder) 84 | 85 | // Set the vertex buffer and any required uniforms 86 | encoder.setVertexBuffer(mesh.vertexBuffers[0].buffer, offset: 0, index: 0) 87 | encoder.setVertexBytes(&uniforms, length: MemoryLayout.stride, index: 3) 88 | 89 | // Draw the mesh using the index buffer 90 | for mtkSubmesh in mesh.submeshes { 91 | encoder.drawIndexedPrimitives(type: mtkSubmesh.primitiveType, 92 | indexCount: mtkSubmesh.indexCount, 93 | indexType: mtkSubmesh.indexType, 94 | indexBuffer: mtkSubmesh.indexBuffer.buffer, 95 | indexBufferOffset: mtkSubmesh.indexBuffer.offset) 96 | } 97 | } 98 | 99 | // MARK: - Utils 100 | private func calculateScale(options: Mesh.Options, projection: Projection) -> SIMD3 { 101 | guard let mesh, options.isResizable else { 102 | return .one 103 | } 104 | let frameSize = calculateSize(options: options, projection: projection) 105 | let meshSize = mesh.size 106 | var scale = frameSize / meshSize 107 | 108 | if let aspectRatio = options.aspectRatio { 109 | let meshAspectRatio = CGFloat(meshSize.x / meshSize.y) 110 | let targetRatio = aspectRatio.0 ?? meshAspectRatio 111 | let frameRation = CGFloat(frameSize.x / frameSize.y) 112 | 113 | switch aspectRatio.1 { 114 | case .fit: 115 | if targetRatio > frameRation { 116 | scale.y = scale.y * Float(frameRation / targetRatio) 117 | } else { 118 | scale.x = scale.x * Float(targetRatio / frameRation) 119 | } 120 | case .fill: 121 | if targetRatio > frameRation { 122 | scale.x = scale.x / Float(frameRation / targetRatio) 123 | } else { 124 | scale.y = scale.y / Float(targetRatio / frameRation) 125 | } 126 | } 127 | } 128 | 129 | return .init(scale.x, scale.y, scale.z) 130 | } 131 | 132 | private func calculateSize(options: Mesh.Options, projection: Projection) -> SIMD3 { 133 | let horizontalInset = options.insets.leading + options.insets.trailing 134 | let verticalInsets = options.insets.top + options.insets.bottom 135 | let frameWidth = options.frame?.width ?? projection.width 136 | let frameHeight = options.frame?.height ?? projection.height 137 | 138 | return .init( 139 | Float(frameWidth - horizontalInset), 140 | Float(frameHeight - verticalInsets), 141 | Float(options.frame?.depth ?? min(frameWidth, frameHeight)) 142 | ) 143 | } 144 | 145 | private func calculatePosition(options: Mesh.Options) -> SIMD3 { 146 | .init( 147 | Float(options.offset.width + options.insets.leading / 2.0 - options.insets.trailing / 2.0), 148 | Float(options.offset.height + options.insets.top / 2.0 - options.insets.bottom / 2.0), 149 | 0.0 150 | ) 151 | } 152 | } 153 | 154 | extension SIMD4 { 155 | 156 | var xyz: SIMD3 { 157 | .init(x, y, z) 158 | } 159 | } 160 | 161 | extension SIMD3 { 162 | 163 | var xy: SIMD2 { 164 | .init(x, y) 165 | } 166 | } 167 | 168 | func float4x4ToFloat3x3(_ matrix: float4x4) -> float3x3 { 169 | float3x3(matrix[0].xyz, matrix[1].xyz, matrix[2].xyz) 170 | } 171 | 172 | func calculateNormalMatrix(_ model: float4x4) -> float3x3 { 173 | float4x4ToFloat3x3(model).inverse.transpose 174 | } 175 | 176 | protocol Updatable { 177 | func update(deltaTime: Double) 178 | } 179 | 180 | protocol Renderable { 181 | func render(encoder: MTLRenderCommandEncoder, 182 | viewProjectionMatrix: float4x4, 183 | viewMatrix: float4x4, 184 | deltaTime: Double) 185 | } 186 | 187 | protocol Transformable { 188 | var position: simd_float3 { get } 189 | var scale: simd_float3 { get } 190 | var rotation: simd_quatf { get } 191 | 192 | var model: float4x4 { get } 193 | } 194 | 195 | extension Transformable { 196 | 197 | var model: float4x4 { 198 | let scaleMatrix = simd_float4x4(diagonal: simd_float4(scale, 1.0)) 199 | var matrix = simd_mul(scaleMatrix, simd_float4x4(rotation)) 200 | matrix.columns.3 = simd_float4(position, 1.0) 201 | return matrix 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Scene/Projection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Oleynik on 29.09.23. 6 | // 7 | 8 | import simd 9 | import CoreGraphics 10 | 11 | struct Projection { 12 | let width: CGFloat 13 | let height: CGFloat 14 | let nearZ: Float 15 | let farZ: Float 16 | 17 | func makeMatrix() -> float4x4 { 18 | Self.orthographicProjection( 19 | left: Float(width / 2.0), 20 | right: Float(-width / 2.0), 21 | bottom: Float(height / 2.0), 22 | top: Float(-height / 2.0), 23 | nearZ: nearZ, 24 | farZ: farZ 25 | ) 26 | } 27 | 28 | private static func orthographicProjection(left: Float, 29 | right: Float, 30 | bottom: Float, 31 | top: Float, 32 | nearZ: Float, 33 | farZ: Float) -> float4x4 { 34 | let scaleX = 2.0 / (right - left) 35 | let scaleY = 2.0 / (top - bottom) 36 | let scaleZ = 1.0 / (nearZ - farZ) 37 | 38 | let offsetX = -(right + left) / (right - left) 39 | let offsetY = -(top + bottom) / (top - bottom) 40 | let offsetZ = nearZ / (nearZ - farZ) 41 | 42 | let matrix = simd_float4x4( 43 | [scaleX, 0, 0, offsetX], 44 | [0, scaleY, 0, offsetY], 45 | [0, 0, scaleZ, offsetZ], 46 | [0, 0, 0, 1] 47 | ) 48 | 49 | return matrix 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Shaders/color_material.metal: -------------------------------------------------------------------------------- 1 | // 2 | // color_material.metal 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 25.03.23. 6 | // 7 | 8 | #include 9 | #include 10 | 11 | #include "material_common.h" 12 | 13 | using namespace metal; 14 | 15 | struct ColorMaterialArguments { 16 | float3 color; 17 | }; 18 | 19 | [[visible]] 20 | float4 color_material(VertexOut inFrag, device ColorMaterialArguments *resource) { 21 | return float4(resource->color, 1.f); 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Shaders/common.metal: -------------------------------------------------------------------------------- 1 | // 2 | // common.metal 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 25.03.23. 6 | // 7 | 8 | #include 9 | #include 10 | 11 | #include "material_common.h" 12 | 13 | using namespace metal; 14 | 15 | struct Vertex { 16 | float4 position [[attribute(0)]]; 17 | float3 normal [[attribute(1)]]; 18 | float3 tangent [[attribute(2)]]; 19 | float2 uv [[attribute(3)]]; 20 | }; 21 | 22 | struct Uniforms { 23 | float4x4 MVP; 24 | float4x4 model; 25 | float4x4 view; 26 | float3x3 normalMatrix; 27 | float time; 28 | }; 29 | 30 | vertex VertexOut vertex_common(Vertex in [[stage_in]], constant Uniforms &uniforms [[buffer(3)]]) { 31 | 32 | float3 displacedPosition = in.position.xyz; 33 | float3 adjustedNormal = in.normal; 34 | float4 pos = float4(displacedPosition, 1.0); 35 | 36 | VertexOut out; 37 | out.position = uniforms.MVP * pos; 38 | out.normal = normalize(uniforms.normalMatrix * adjustedNormal); 39 | out.tangent = normalize(uniforms.normalMatrix * in.tangent); 40 | out.vNormal = normalize(uniforms.view * float4(uniforms.normalMatrix * adjustedNormal, 0.f)).xyz; 41 | out.vTangent = (uniforms.view * float4(uniforms.normalMatrix * in.tangent, 0.f)).xyz; 42 | out.worldPos = (uniforms.model * pos).xyz; 43 | out.uv = in.uv; 44 | return out; 45 | } 46 | 47 | // MARK: - Fragment 48 | 49 | using MaterialFunction = float4(VertexOut, device void *); 50 | 51 | constant unsigned int resourcesStride [[function_constant(0)]]; 52 | constant int materialsCount [[function_constant(1)]]; 53 | 54 | struct Material { 55 | int functionIndex; 56 | int resourceIndex; 57 | int blend; 58 | float alpha; 59 | }; 60 | 61 | // Normal Blend 62 | float4 normalBlend(float4 src, float4 dst) { 63 | return src * src.a + dst * (1.0 - src.a); 64 | } 65 | 66 | // Multiply Blend 67 | float4 multiplyBlend(float4 src, float4 dst) { 68 | return src * dst; 69 | } 70 | 71 | // Screen Blend 72 | float4 screenBlend(float4 src, float4 dst) { 73 | return 1.0 - (1.0 - src) * (1.0 - dst); 74 | } 75 | 76 | // Overlay Blend 77 | float4 overlayBlend(float4 src, float4 dst) { 78 | float3 resultRGB; 79 | resultRGB = metal::select( 80 | 2.0 * src.rgb * dst.rgb, 81 | 1.0 - 2.0 * (1.0 - src.rgb) * (1.0 - dst.rgb), 82 | dst.rgb > 0.5 83 | ); 84 | return float4(resultRGB, src.a + dst.a - src.a * dst.a); 85 | } 86 | 87 | fragment float4 fragment_common(VertexOut inFrag [[stage_in]], 88 | visible_function_table materialFunctions [[buffer(0)]], 89 | device void *resources [[buffer(1)]], 90 | device Material *materials [[buffer(2)]]) { 91 | float4 finalColor = float4(0.f, 0.f, 0.f, 1.f); 92 | 93 | for (int i = 0; i < materialsCount; ++i) { 94 | Material material = materials[i]; 95 | MaterialFunction *materialFunction = materialFunctions[material.functionIndex]; 96 | device void *resource = ((device char *)resources + resourcesStride * material.resourceIndex); 97 | 98 | [[function_groups("material")]] float4 materialColor = materialFunction(inFrag, resource); 99 | materialColor.a *= material.alpha; 100 | 101 | switch (material.blend) { 102 | case 0: 103 | finalColor = normalBlend(materialColor, finalColor); 104 | break; 105 | case 1: 106 | finalColor = multiplyBlend(materialColor, finalColor); 107 | break; 108 | case 2: 109 | finalColor = screenBlend(materialColor, finalColor); 110 | break; 111 | case 3: 112 | finalColor = overlayBlend(materialColor, finalColor); 113 | break; 114 | } 115 | } 116 | return finalColor; 117 | } 118 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Shaders/fresnel_material.metal: -------------------------------------------------------------------------------- 1 | // 2 | // matcap_material.metal 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 25.03.23. 6 | // 7 | 8 | #include 9 | #include 10 | 11 | #include "material_common.h" 12 | 13 | using namespace metal; 14 | 15 | struct FresnelMaterialArgument { 16 | float3 color; 17 | float intensity; 18 | float scale; 19 | float bias; 20 | }; 21 | 22 | float FresnelTerm(float cosTheta, float3 f0, float fresnelPower) { 23 | return dot(f0, pow(1.0 - cosTheta, fresnelPower)); 24 | } 25 | 26 | [[visible]] 27 | float4 fresnel_material(VertexOut inFrag, device FresnelMaterialArgument *data) { 28 | float3 cameraPosition = float3(0.0, 0.f, -1000.f); 29 | float3 N = normalize(inFrag.normal); 30 | float3 V = normalize(cameraPosition - inFrag.worldPos); 31 | 32 | float3 f0 = 0.04; 33 | float3 fresnelColor = data->color; 34 | float cosPhi = dot(N, V); 35 | float term = FresnelTerm(cosPhi, f0, data->intensity); 36 | return float4(fresnelColor, (data->bias + (1.f - data->bias) * term) * data->scale); 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Shaders/matcap_material.metal: -------------------------------------------------------------------------------- 1 | // 2 | // matcap_material.metal 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 25.03.23. 6 | // 7 | 8 | #include 9 | #include 10 | 11 | #include "material_common.h" 12 | 13 | using namespace metal; 14 | 15 | struct MatcapMaterialArgument { 16 | texture2d texture; 17 | }; 18 | 19 | [[visible]] 20 | float4 matcap_material(VertexOut inFrag, device MatcapMaterialArgument *data) { 21 | constexpr sampler smp(mag_filter::bicubic, min_filter::bicubic); 22 | 23 | float3 bitangent = cross(inFrag.vNormal, inFrag.vTangent); 24 | float3x3 TBN = float3x3(inFrag.vTangent, bitangent, inFrag.vNormal); 25 | float3 N = normalize(TBN * inFrag.vNormal); 26 | 27 | float2 uv = float2(-N.x, N.y) * 0.5 + 0.5; 28 | float4 matcapColor = data->texture.sample(smp, uv); 29 | return matcapColor; 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Shaders/material_common.h: -------------------------------------------------------------------------------- 1 | // 2 | // Header.h 3 | // 4 | // 5 | // Created by Alexey Oleynik on 30.09.23. 6 | // 7 | 8 | #pragma once 9 | 10 | struct VertexOut { 11 | float4 position [[position]]; 12 | float3 normal; 13 | float3 tangent; 14 | float3 vNormal; 15 | float3 vTangent; 16 | float3 worldPos; 17 | float2 uv; 18 | }; 19 | -------------------------------------------------------------------------------- /Sources/Creamy3D/Shaders/texture_material.metal: -------------------------------------------------------------------------------- 1 | // 2 | // matcap_material.metal 3 | // Creamy3D 4 | // 5 | // Created by Alexey Oleynik on 25.03.23. 6 | // 7 | 8 | #include 9 | #include 10 | 11 | #include "material_common.h" 12 | 13 | using namespace metal; 14 | 15 | struct TextureMaterialArgument { 16 | texture2d texture; 17 | }; 18 | 19 | [[visible]] 20 | float4 texture_material(VertexOut inFrag, device TextureMaterialArgument *data) { 21 | constexpr sampler smp(mag_filter::bicubic, min_filter::bicubic); 22 | 23 | float4 matcapColor = data->texture.sample(smp, inFrag.uv); 24 | return matcapColor; 25 | } 26 | -------------------------------------------------------------------------------- /Tests/Creamy3DTests/Creamy3DTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Creamy3D 3 | 4 | final class Creamy3DTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | --------------------------------------------------------------------------------