├── .github └── FUNDING.yml ├── Game ├── Test.tiff ├── header.h ├── game.c └── game.swift ├── examples └── basic_shapes.png ├── metalRay Shared ├── Fonts │ ├── Square.tiff │ ├── OpenSans.tiff │ ├── OpenSans.json │ └── Square.json ├── Assets.xcassets │ ├── Contents.json │ ├── tvOS App Icon & Top Shelf Image.brandassets │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Top Shelf Image.imageset │ │ │ └── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ColorMap.textureset │ │ ├── Universal.mipmapset │ │ │ ├── ColorMap.png │ │ │ └── Contents.json │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Draw │ ├── Math.swift │ ├── MetalStates.swift │ ├── Draw.metal │ └── MetalDraw2D.swift ├── metalray.h ├── ShaderTypes.h ├── Functions │ ├── Core.swift │ └── Drawing.swift ├── Shaders.metal ├── Globals.swift ├── Bridge.h ├── metalray_core.h ├── Misc │ ├── Misc.swift │ └── Font.swift ├── RayView.swift └── Game.swift ├── metalRay.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── markusmoenig.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── metalRay macOS ├── metalRay_macOS.entitlements ├── AppDelegate.swift └── GameViewController.swift ├── LICENSE ├── metalRay iOS ├── GameViewController.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── AppDelegate.swift ├── metalRay tvOS ├── GameViewController.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── AppDelegate.swift └── Readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: markusmoenig 2 | patreon: markusmoenig 3 | -------------------------------------------------------------------------------- /Game/Test.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusmoenig/metalRay/HEAD/Game/Test.tiff -------------------------------------------------------------------------------- /examples/basic_shapes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusmoenig/metalRay/HEAD/examples/basic_shapes.png -------------------------------------------------------------------------------- /metalRay Shared/Fonts/Square.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusmoenig/metalRay/HEAD/metalRay Shared/Fonts/Square.tiff -------------------------------------------------------------------------------- /metalRay Shared/Fonts/OpenSans.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusmoenig/metalRay/HEAD/metalRay Shared/Fonts/OpenSans.tiff -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Game/header.h: -------------------------------------------------------------------------------- 1 | // 2 | // common.h 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 24/8/23. 6 | // 7 | 8 | #ifndef common_h 9 | #define common_h 10 | 11 | #endif /* common_h */ 12 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/ColorMap.textureset/Universal.mipmapset/ColorMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusmoenig/metalRay/HEAD/metalRay Shared/Assets.xcassets/ColorMap.textureset/Universal.mipmapset/ColorMap.png -------------------------------------------------------------------------------- /metalRay.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /metalRay.xcodeproj/xcuserdata/markusmoenig.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/ColorMap.textureset/Universal.mipmapset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "levels" : [ 7 | { 8 | "filename" : "ColorMap.png", 9 | "mipmap-level" : "base" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /metalRay.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /metalRay macOS/metalRay_macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/ColorMap.textureset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "properties" : { 7 | "origin" : "bottom-left", 8 | "interpretation" : "non-premultiplied-colors" 9 | }, 10 | "textures" : [ 11 | { 12 | "idiom" : "universal", 13 | "filename" : "Universal.mipmapset" 14 | } 15 | ] 16 | } 17 | 18 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /metalRay Shared/Draw/Math.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Math.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import simd 9 | 10 | typealias float2 = SIMD2 11 | typealias float3 = SIMD3 12 | typealias float4 = SIMD4 13 | 14 | let π = Float.pi 15 | 16 | extension Float { 17 | var radiansToDegrees: Float { 18 | (self / π) * 180 19 | } 20 | var degreesToRadians: Float { 21 | (self / 180) * π 22 | } 23 | } 24 | 25 | extension Double { 26 | var radiansToDegrees: Double { 27 | (self / Double.pi) * 180 28 | } 29 | var degreesToRadians: Double { 30 | (self / 180) * Double.pi 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /metalRay macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // metalRay macOS 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | // Insert code here to initialize your application 17 | } 18 | 19 | func applicationWillTerminate(_ aNotification: Notification) { 20 | // Insert code here to tear down your application 21 | } 22 | 23 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 24 | return true 25 | } 26 | 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Game/game.c: -------------------------------------------------------------------------------- 1 | // 2 | // main.c 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | #include 9 | #include 10 | 11 | int textureId; 12 | float rot = 0.0; 13 | 14 | void InitGame(void) { 15 | textureId = LoadTexture("Test"); 16 | 17 | //Vector2 size = GetFontSize("MetalRay", 10.0); 18 | } 19 | 20 | void UpdateGame(void) { 21 | 22 | rot += 1.0; 23 | 24 | BeginDrawing(); 25 | Clear(ORANGE); 26 | 27 | SetTexture(textureId); 28 | DrawRectRotCenter((Rectangle){100, 100, 400, 400}, GREEN, rot); 29 | SetTexture(0); 30 | DrawText((Vector2){ 100, 100}, "METALRAY", 60.0, GREEN); 31 | EndDrawing(); 32 | } 33 | 34 | void DeinitGame(void) { 35 | } 36 | -------------------------------------------------------------------------------- /metalRay.xcodeproj/xcuserdata/markusmoenig.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | metalRay iOS.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | metalRay macOS.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | metalRay tvOS.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 1 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Markus Moenig 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /metalRay Shared/metalray.h: -------------------------------------------------------------------------------- 1 | // 2 | // metalray.h 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 28/8/23. 6 | // 7 | 8 | #include "metalray_core.h" 9 | 10 | #ifndef metalray_h 11 | #define metalray_h 12 | 13 | #include 14 | 15 | // Device 16 | 17 | extern Vector2 GetScreenSize(void); 18 | 19 | extern bool HasTouch(void); 20 | extern bool HasTap(void); 21 | extern bool HasDoubleTap(void); 22 | extern bool HasTouchEnded(void); 23 | extern Vector2 GetTouchPos(void); 24 | 25 | // Drawing 26 | 27 | extern void BeginDrawing(void); 28 | extern void EndDrawing(void); 29 | 30 | extern void Clear(Color); 31 | 32 | extern void DrawRect(Rectangle rect, Color color); 33 | extern void DrawRectRotCenter(Rectangle rect, Color color, float rot); 34 | 35 | extern void SetFont(char *fontname); 36 | extern Vector2 GetFontSize(char *text, float size); 37 | extern void DrawText(Vector2 pos, char *text, float size, Color color); 38 | 39 | // Textures 40 | 41 | extern int CreateTexture(int width, int height); 42 | 43 | extern int LoadTexture(char *texture); 44 | 45 | extern bool SetTexture(int id); 46 | extern bool SetTarget(int id); 47 | 48 | #endif /* metalray_h */ 49 | -------------------------------------------------------------------------------- /metalRay Shared/ShaderTypes.h: -------------------------------------------------------------------------------- 1 | // 2 | // ShaderTypes.h 3 | // metalRay Shared 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | // 9 | // Header containing types and enum constants shared between Metal shaders and Swift/ObjC source 10 | // 11 | #ifndef ShaderTypes_h 12 | #define ShaderTypes_h 13 | 14 | #ifdef __METAL_VERSION__ 15 | #define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type 16 | typedef metal::int32_t EnumBackingType; 17 | #else 18 | #import 19 | typedef NSInteger EnumBackingType; 20 | #endif 21 | 22 | #include 23 | 24 | typedef NS_ENUM(EnumBackingType, BufferIndex) 25 | { 26 | BufferIndexMeshPositions = 0, 27 | BufferIndexMeshGenerics = 1, 28 | BufferIndexUniforms = 2 29 | }; 30 | 31 | typedef NS_ENUM(EnumBackingType, VertexAttribute) 32 | { 33 | VertexAttributePosition = 0, 34 | VertexAttributeTexcoord = 1, 35 | }; 36 | 37 | typedef NS_ENUM(EnumBackingType, TextureIndex) 38 | { 39 | TextureIndexColor = 0, 40 | }; 41 | 42 | typedef struct 43 | { 44 | matrix_float4x4 projectionMatrix; 45 | matrix_float4x4 modelViewMatrix; 46 | } Uniforms; 47 | 48 | #endif /* ShaderTypes_h */ 49 | 50 | -------------------------------------------------------------------------------- /metalRay iOS/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // metalRay iOS 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import UIKit 9 | import MetalKit 10 | 11 | // Our iOS specific view controller 12 | class GameViewController: UIViewController { 13 | 14 | var rayView : RayView! 15 | var game : Game! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | guard let rayView = self.view as? RayView else { 21 | print("View attached to GameViewController is not an MTKView") 22 | return 23 | } 24 | 25 | // Select the device to render with. We choose the default device 26 | guard let defaultDevice = MTLCreateSystemDefaultDevice() else { 27 | print("Metal is not supported on this device") 28 | return 29 | } 30 | 31 | rayView.device = defaultDevice 32 | 33 | guard let game = Game(view: rayView) else { 34 | print("Game cannot be initialized") 35 | return 36 | } 37 | 38 | game.mtkView(rayView, drawableSizeWillChange: rayView.drawableSize) 39 | rayView.platformInit() 40 | 41 | rayView.delegate = game 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /metalRay tvOS/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // metalRay tvOS 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import UIKit 9 | import MetalKit 10 | 11 | // Our iOS specific view controller 12 | class GameViewController: UIViewController { 13 | 14 | var rayView : RayView! 15 | var game : Game! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | guard let rayView = self.view as? RayView else { 21 | print("View attached to GameViewController is not an MTKView") 22 | return 23 | } 24 | 25 | // Select the device to render with. We choose the default device 26 | guard let defaultDevice = MTLCreateSystemDefaultDevice() else { 27 | print("Metal is not supported on this device") 28 | return 29 | } 30 | 31 | rayView.device = defaultDevice 32 | 33 | guard let game = Game(view: rayView) else { 34 | print("Game cannot be initialized") 35 | return 36 | } 37 | 38 | game.mtkView(rayView, drawableSizeWillChange: rayView.drawableSize) 39 | rayView.platformInit() 40 | 41 | rayView.delegate = game 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /metalRay macOS/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // metalRay macOS 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import Cocoa 9 | import MetalKit 10 | 11 | // Our macOS specific view controller 12 | class GameViewController: NSViewController { 13 | 14 | var rayView : RayView! 15 | var game : Game! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | guard let rayView = self.view as? RayView else { 21 | print("View attached to GameViewController is not an MTKView") 22 | return 23 | } 24 | 25 | // Select the device to render with. We choose the default device 26 | guard let defaultDevice = MTLCreateSystemDefaultDevice() else { 27 | print("Metal is not supported on this device") 28 | return 29 | } 30 | 31 | rayView.device = defaultDevice 32 | 33 | guard let game = Game(view: rayView) else { 34 | print("Game cannot be initialized") 35 | return 36 | } 37 | 38 | game.mtkView(rayView, drawableSizeWillChange: rayView.drawableSize) 39 | rayView.platformInit() 40 | 41 | rayView.delegate = game 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /metalRay Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /metalRay Shared/Functions/Core.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Core.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 22/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @_silgen_name("GetScreenSize") 11 | func GetScreenSize() -> Vector2 { 12 | if let game = globalGame { 13 | return Vector2(x: game.draw2D.viewSize.x, y: game.draw2D.viewSize.y) 14 | } 15 | return Vector2(x: 0, y: 0) 16 | } 17 | 18 | @_silgen_name("HasTouch") 19 | func HasTouch() -> Bool { 20 | if let game = globalGame { 21 | return game.rayView.mouseIsDown 22 | } 23 | return false 24 | } 25 | 26 | @_silgen_name("GetTouchPos") 27 | func GetTouchPos() -> Vector2 { 28 | if let game = globalGame { 29 | return Vector2(x: game.rayView.mousePos.x, y: game.rayView.mousePos.y) 30 | } 31 | return Vector2(x: 0, y: 0) 32 | } 33 | 34 | @_silgen_name("HasTap") 35 | func HasTap() -> Bool { 36 | if let game = globalGame { 37 | return game.rayView.hasTap 38 | } 39 | return false 40 | } 41 | 42 | @_silgen_name("HasDoubleTap") 43 | func HasDoubleTap() -> Bool { 44 | if let game = globalGame { 45 | return game.rayView.hasDoubleTap 46 | } 47 | return false 48 | } 49 | 50 | @_silgen_name("HasTouchEnded") 51 | func HasTouchEnded() -> Bool { 52 | if let game = globalGame { 53 | return game.rayView.hasTouchEnded 54 | } 55 | return false 56 | } 57 | -------------------------------------------------------------------------------- /metalRay tvOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /metalRay Shared/Shaders.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Shaders.metal 3 | // metalRay Shared 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | // File for Metal kernel and shader functions 9 | 10 | #include 11 | #include 12 | 13 | // Including header shared between this Metal shader code and Swift/C code executing Metal API commands 14 | #import "ShaderTypes.h" 15 | 16 | using namespace metal; 17 | 18 | typedef struct 19 | { 20 | float3 position [[attribute(VertexAttributePosition)]]; 21 | float2 texCoord [[attribute(VertexAttributeTexcoord)]]; 22 | } Vertex; 23 | 24 | typedef struct 25 | { 26 | float4 position [[position]]; 27 | float2 texCoord; 28 | } ColorInOut; 29 | 30 | vertex ColorInOut vertexShader(Vertex in [[stage_in]], 31 | constant Uniforms & uniforms [[ buffer(BufferIndexUniforms) ]]) 32 | { 33 | ColorInOut out; 34 | 35 | float4 position = float4(in.position, 1.0); 36 | out.position = uniforms.projectionMatrix * uniforms.modelViewMatrix * position; 37 | out.texCoord = in.texCoord; 38 | 39 | return out; 40 | } 41 | 42 | fragment float4 fragmentShader(ColorInOut in [[stage_in]], 43 | constant Uniforms & uniforms [[ buffer(BufferIndexUniforms) ]], 44 | texture2d colorMap [[ texture(TextureIndexColor) ]]) 45 | { 46 | constexpr sampler colorSampler(mip_filter::linear, 47 | mag_filter::linear, 48 | min_filter::linear); 49 | 50 | half4 colorSample = colorMap.sample(colorSampler, in.texCoord.xy); 51 | 52 | return float4(colorSample); 53 | } 54 | -------------------------------------------------------------------------------- /Game/game.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | var textureId : Int = -1 11 | var rot : Float = 0.0 12 | 13 | func initGame() { 14 | // Call the C Init, remove if not needed 15 | InitGame(); 16 | 17 | //textureId = LoadTexture(name: toCStr(string: "Test")) 18 | 19 | // Create a texture and draw a rectangle and text in it 20 | 21 | textureId = CreateTexture(width: 400, height: 400) 22 | 23 | SetTarget(id: textureId) 24 | BeginDrawing() 25 | Clear(color: YELLOW) 26 | DrawRect(rect: Rectangle(x: 100, y: 200, width: 200, height: 200), color: BLUE) 27 | DrawText(pos: Vector2(x: 50 , y: 100), text: toCStr(string: "metalRay"), size: 150.0, color: PINK) 28 | EndDrawing() 29 | SetTarget(id: 0) 30 | } 31 | 32 | func updateGame() { 33 | 34 | // Call the C Update, remove if not needed 35 | //UpdateGame(); 36 | 37 | // SetTarget(id: textureId) 38 | // BeginDrawing() 39 | // Clear(color: YELLOW) 40 | // DrawRect(rect: Rectangle(x: 100, y: 200, width: 200, height: 200), color: BLUE) 41 | // DrawText(pos: Vector2(x: 100.0 , y: 100.0), text: toCStr(string: "metalRay"), size: 150.0, color: PINK) 42 | // EndDrawing() 43 | // SetTarget(id: 0) 44 | // 45 | rot += 1 46 | BeginDrawing() 47 | Clear(color: ORANGE) 48 | 49 | SetTexture(id: textureId) 50 | DrawRectRotCenter(rect: Rectangle(x: 100, y: 100, width: 400, height: 400), color: GREEN, rot: rot) 51 | SetTexture(id: 0) 52 | EndDrawing() 53 | } 54 | 55 | func deinitGame() { 56 | // Call the C Deinit, remove if not needed 57 | DeinitGame(); 58 | } 59 | -------------------------------------------------------------------------------- /metalRay iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /metalRay Shared/Globals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Globals.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | let LIGHTGRAY = Color(r: 200, g: 200, b: 200, a: 255) 11 | let GRAY = Color(r: 130, g: 130, b: 130, a: 255) 12 | let DARKGRAY = Color(r: 80, g: 80, b: 80, a: 255) 13 | let YELLOW = Color(r: 253, g: 249, b: 0, a: 255) 14 | let GOLD = Color(r: 255, g: 203, b: 0, a: 255) 15 | let ORANGE = Color(r: 255, g: 161, b: 0, a: 255) 16 | let PINK = Color(r: 255, g: 109, b: 194, a: 255) 17 | let RED = Color(r: 230, g: 41, b: 55, a: 255) 18 | let MAROON = Color(r: 190, g: 33, b: 55, a: 255) 19 | let GREEN = Color(r: 0, g: 228, b: 48, a: 255) 20 | let LIME = Color(r: 0, g: 158, b: 47, a: 255) 21 | let DARKGREEN = Color(r: 0, g: 117, b: 44, a: 255) 22 | let SKYBLUE = Color(r: 102, g: 191, b: 255, a: 255) 23 | let BLUE = Color(r: 0, g: 121, b: 241, a: 255) 24 | let DARKBLUE = Color(r: 0, g: 82, b: 172, a: 255) 25 | let PURPLE = Color(r: 200, g: 122, b: 255, a: 255) 26 | let VIOLET = Color(r: 135, g: 60, b: 190, a: 255) 27 | let DARKPURPLE = Color(r: 112, g: 31, b: 126, a: 255) 28 | let BEIGE = Color(r: 211, g: 176, b: 131, a: 255) 29 | let BROWN = Color(r: 127, g: 106, b: 79, a: 255) 30 | let DARKBROWN = Color(r: 76, g: 63, b: 47, a: 255) 31 | let WHITE = Color(r: 255, g: 255, b: 255, a: 255) 32 | let BLACK = Color(r: 0, g: 0, b: 0, a: 255) 33 | let BLANK = Color(r: 0, g: 0, b: 0, a: 0) 34 | let MAGENTA = Color(r: 255, g: 0, b: 255, a: 255) 35 | let RAYWHITE = Color(r: 245, g: 245, b: 245, a: 255) 36 | 37 | func float4ToColor(_ color: float4) -> Color { 38 | return Color(r: UInt8(color.x * 255), g: UInt8(color.y * 255), b: UInt8(color.z * 255), a: UInt8(color.w * 255)) 39 | } 40 | 41 | func colorToFloat4(_ color: Color) -> float4 { 42 | return float4(Float(color.r) / 255, Float(color.g) / 255, Float(color.b) / 255, Float(color.a) / 255) 43 | } 44 | 45 | func vector2ToFloat2(_ v: Vector2) -> float2 { 46 | return float2(v.x, v.y) 47 | } 48 | 49 | func toCStr(string: String) -> UnsafePointer { 50 | return (string as NSString).utf8String! 51 | } 52 | -------------------------------------------------------------------------------- /metalRay tvOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /metalRay iOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /metalRay iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // metalRay iOS 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /metalRay tvOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // metalRay tvOS 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /metalRay Shared/Bridge.h: -------------------------------------------------------------------------------- 1 | // 2 | // Metal.h 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | #ifndef Metal_h 9 | #define Metal_h 10 | 11 | #include 12 | 13 | typedef struct 14 | { 15 | vector_float2 position; 16 | vector_float2 textureCoordinate; 17 | } VertexUniform; 18 | 19 | typedef struct 20 | { 21 | vector_float2 screenSize; 22 | vector_float2 pos; 23 | vector_float2 size; 24 | float globalAlpha; 25 | 26 | } TextureUniform; 27 | 28 | typedef struct 29 | { 30 | vector_float4 fillColor; 31 | vector_float4 borderColor; 32 | float radius; 33 | float borderSize; 34 | float rotation; 35 | float onion; 36 | 37 | int hasTexture; 38 | vector_float2 textureSize; 39 | } DiscUniform; 40 | 41 | typedef struct 42 | { 43 | vector_float2 screenSize; 44 | vector_float2 pos; 45 | vector_float2 size; 46 | float round; 47 | float borderSize; 48 | vector_float4 fillColor; 49 | vector_float4 borderColor; 50 | float rotation; 51 | float onion; 52 | 53 | int hasTexture; 54 | vector_float2 textureSize; 55 | } BoxUniform; 56 | 57 | typedef struct 58 | { 59 | int hasTexture; 60 | } RectUniform; 61 | 62 | typedef struct 63 | { 64 | vector_float2 screenSize; 65 | vector_float2 offset; 66 | float gridSize; 67 | vector_float4 backColor; 68 | vector_float4 gridColor; 69 | } GridUniform; 70 | 71 | typedef struct 72 | { 73 | vector_float2 size; 74 | vector_float2 sp, ep; 75 | float width, borderSize; 76 | vector_float4 fillColor; 77 | vector_float4 borderColor; 78 | 79 | } LineUniform; 80 | 81 | typedef struct 82 | { 83 | vector_float2 size; 84 | vector_float2 p1, p2, p3; 85 | float width, borderSize; 86 | vector_float4 fillColor; 87 | vector_float4 borderColor; 88 | 89 | } BezierUniform; 90 | 91 | typedef struct 92 | { 93 | vector_float2 atlasSize; 94 | vector_float2 fontPos; 95 | vector_float2 fontSize; 96 | } TextUniform; 97 | 98 | typedef struct 99 | { 100 | float time; 101 | unsigned int frame; 102 | } MetalData; 103 | 104 | typedef struct 105 | { 106 | float time; 107 | unsigned int frame; 108 | } NoiseData; 109 | 110 | typedef struct 111 | { 112 | vector_float2 screenSize; 113 | 114 | vector_float3 camOrigin; 115 | vector_float3 camCenter; 116 | float camFov; 117 | 118 | vector_int3 tileMin; 119 | vector_int3 tileMax; 120 | vector_int3 tileSize; 121 | 122 | int tileDensity; 123 | 124 | } VoxelTileUniform; 125 | 126 | typedef struct { 127 | 128 | // Density of the tile 129 | int density; 130 | // World coordinate of the tile 131 | vector_float3 coord; 132 | // Number of shapes we have data for 133 | int shapesCount; 134 | 135 | } ModelerUniform; 136 | 137 | void InitGame(void); 138 | void UpdateGame(void); 139 | void DeinitGame(void); 140 | 141 | #include "metalray_core.h" 142 | #include "../Game/header.h" 143 | 144 | #endif /* Metal_h */ 145 | -------------------------------------------------------------------------------- /metalRay Shared/metalray_core.h: -------------------------------------------------------------------------------- 1 | // 2 | // metalray.h 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 28/8/23. 6 | // 7 | 8 | #ifndef metalray_core_h 9 | #define metalray_core_h 10 | 11 | void InitGame(void); 12 | void UpdateGame(void); 13 | void DeinitGame(void); 14 | 15 | // Some of the below is taken from raylib.h, under the zlib license and copyright by 16 | 17 | // Globals 18 | 19 | #define CLITERAL(type) (type) 20 | 21 | // Color, 4 components, R8G8B8A8 (32bit) 22 | typedef struct Color { 23 | unsigned char r; // Color red value 24 | unsigned char g; // Color green value 25 | unsigned char b; // Color blue value 26 | unsigned char a; // Color alpha value 27 | } Color; 28 | 29 | // Vec2, 2 components 30 | typedef struct Vector2 { 31 | float x; // Vector x component 32 | float y; // Vector y component 33 | } Vector2; 34 | 35 | // Vec3, 3 components 36 | typedef struct Vector3 { 37 | float x; // Vector x component 38 | float y; // Vector y component 39 | float z; // Vector z component 40 | } Vector3; 41 | 42 | // Vec4, 4 components 43 | typedef struct Vector4 { 44 | float x; // Vector x component 45 | float y; // Vector y component 46 | float z; // Vector z component 47 | float w; // Vector w component 48 | } Vector4; 49 | 50 | // Rectangle, 4 components 51 | typedef struct Rectangle { 52 | float x; // Rectangle top-left corner position x 53 | float y; // Rectangle top-left corner position y 54 | float width; // Rectangle width 55 | float height; // Rectangle height 56 | } Rectangle; 57 | 58 | // Enums 59 | 60 | // Some Basic Colors 61 | // NOTE: Custom raylib color palette for amazing visuals on WHITE background 62 | #define LIGHTGRAY CLITERAL(Color){ 200, 200, 200, 255 } // Light Gray 63 | #define GRAY CLITERAL(Color){ 130, 130, 130, 255 } // Gray 64 | #define DARKGRAY CLITERAL(Color){ 80, 80, 80, 255 } // Dark Gray 65 | #define YELLOW CLITERAL(Color){ 253, 249, 0, 255 } // Yellow 66 | #define GOLD CLITERAL(Color){ 255, 203, 0, 255 } // Gold 67 | #define ORANGE CLITERAL(Color){ 255, 161, 0, 255 } // Orange 68 | #define PINK CLITERAL(Color){ 255, 109, 194, 255 } // Pink 69 | #define RED CLITERAL(Color){ 230, 41, 55, 255 } // Red 70 | #define MAROON CLITERAL(Color){ 190, 33, 55, 255 } // Maroon 71 | #define GREEN CLITERAL(Color){ 0, 228, 48, 255 } // Green 72 | #define LIME CLITERAL(Color){ 0, 158, 47, 255 } // Lime 73 | #define DARKGREEN CLITERAL(Color){ 0, 117, 44, 255 } // Dark Green 74 | #define SKYBLUE CLITERAL(Color){ 102, 191, 255, 255 } // Sky Blue 75 | #define BLUE CLITERAL(Color){ 0, 121, 241, 255 } // Blue 76 | #define DARKBLUE CLITERAL(Color){ 0, 82, 172, 255 } // Dark Blue 77 | #define PURPLE CLITERAL(Color){ 200, 122, 255, 255 } // Purple 78 | #define VIOLET CLITERAL(Color){ 135, 60, 190, 255 } // Violet 79 | #define DARKPURPLE CLITERAL(Color){ 112, 31, 126, 255 } // Dark Purple 80 | #define BEIGE CLITERAL(Color){ 211, 176, 131, 255 } // Beige 81 | #define BROWN CLITERAL(Color){ 127, 106, 79, 255 } // Brown 82 | #define DARKBROWN CLITERAL(Color){ 76, 63, 47, 255 } // Dark Brown 83 | 84 | #define WHITE CLITERAL(Color){ 255, 255, 255, 255 } // White 85 | #define BLACK CLITERAL(Color){ 0, 0, 0, 255 } // Black 86 | #define BLANK CLITERAL(Color){ 0, 0, 0, 0 } // Blank (Transparent) 87 | #define MAGENTA CLITERAL(Color){ 255, 0, 255, 255 } // Magenta 88 | #define RAYWHITE CLITERAL(Color){ 245, 245, 245, 255 } // My own White (raylib logo) 89 | 90 | #endif /* metalray_h */ 91 | -------------------------------------------------------------------------------- /metalRay Shared/Misc/Misc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Misc.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Rect class 11 | class MRRect 12 | { 13 | var x : Float 14 | var y: Float 15 | var width: Float 16 | var height: Float 17 | 18 | init( _ x : Float, _ y : Float, _ width: Float, _ height : Float, scale: Float = 1 ) 19 | { 20 | self.x = x * scale; self.y = y * scale; self.width = width * scale; self.height = height * scale 21 | } 22 | 23 | init() 24 | { 25 | x = 0; y = 0; width = 0; height = 0 26 | } 27 | 28 | init(_ rect : MRRect) 29 | { 30 | x = rect.x; y = rect.y 31 | width = rect.width; height = rect.height 32 | } 33 | 34 | func set( _ x : Float, _ y : Float, _ width: Float, _ height : Float, scale: Float = 1 ) 35 | { 36 | self.x = x * scale; self.y = y * scale; self.width = width * scale; self.height = height * scale 37 | } 38 | 39 | /// Copy the content of the given rect 40 | func copy(_ rect : MRRect) 41 | { 42 | x = rect.x; y = rect.y 43 | width = rect.width; height = rect.height 44 | } 45 | 46 | /// Returns true if the given point is inside the rect 47 | func contains( _ x : Float, _ y : Float ) -> Bool 48 | { 49 | if self.x <= x && self.y <= y && self.x + self.width >= x && self.y + self.height >= y { 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | /// Returns true if the given point is inside the scaled rect 56 | func contains( _ x : Float, _ y : Float, _ scale : Float ) -> Bool 57 | { 58 | if self.x <= x && self.y <= y && self.x + self.width * scale >= x && self.y + self.height * scale >= y { 59 | return true; 60 | } 61 | return false; 62 | } 63 | 64 | /// Intersect the rects 65 | func intersect(_ rect: MRRect) 66 | { 67 | let left = max(x, rect.x) 68 | let top = max(y, rect.y) 69 | let right = min(x + width, rect.x + rect.width ) 70 | let bottom = min(y + height, rect.y + rect.height ) 71 | let width = right - left 72 | let height = bottom - top 73 | 74 | if width > 0 && height > 0 { 75 | x = left 76 | y = top 77 | self.width = width 78 | self.height = height 79 | } else { 80 | copy(rect) 81 | } 82 | } 83 | 84 | /// Merge the rects 85 | func merge(_ rect: MRRect) 86 | { 87 | width = width > rect.width ? width : rect.width + (rect.x - x) 88 | height = height > rect.height ? height : rect.height + (rect.y - y) 89 | x = min(x, rect.x) 90 | y = min(y, rect.y) 91 | } 92 | 93 | /// Returns the cordinate of the right edge of the rectangle 94 | func right() -> Float 95 | { 96 | return x + width 97 | } 98 | 99 | /// Returns the cordinate of the bottom of the rectangle 100 | func bottom() -> Float 101 | { 102 | return y + height 103 | } 104 | 105 | /// Shrinks the rectangle by the given x and y amounts 106 | func shrink(_ x : Float,_ y : Float) 107 | { 108 | self.x += x 109 | self.y += y 110 | self.width -= x * 2 111 | self.height -= y * 2 112 | } 113 | 114 | /// Clears the rect 115 | func clear() 116 | { 117 | set(0, 0, 0, 0) 118 | } 119 | 120 | /// Returns the position of the rect as float2 121 | func position() -> float2 122 | { 123 | return float2(x, y) 124 | } 125 | 126 | /// Returns the middle of the rect 127 | func middle() -> float2 128 | { 129 | return float2(x, y) + float2(width, height) / 2.0 130 | } 131 | 132 | /// Returns the size of the rect as float2 133 | func size() -> float2 134 | { 135 | return float2(width, height) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /metalRay Shared/Functions/Drawing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drawing.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @_silgen_name("BeginDrawing") 11 | func BeginDrawing() { 12 | if let game = globalGame { 13 | game.draw2D.encodeStart() 14 | } 15 | } 16 | 17 | @_silgen_name("EndDrawing") 18 | func EndDrawing() { 19 | if let game = globalGame { 20 | game.draw2D.encodeEnd() 21 | } 22 | } 23 | 24 | /// Clear 25 | @_silgen_name("Clear") 26 | func Clear(color: Color) { 27 | if let game = globalGame { 28 | 29 | let size = game.draw2D.viewSize 30 | let c = colorToFloat4(color) 31 | 32 | game.draw2D.startShape(type: .triangle) 33 | game.draw2D.addVertex(float2(size.x, size.y), float2(1.0, 0.0), c) 34 | game.draw2D.addVertex(float2(0, size.y), float2(0.0, 0.0), c) 35 | game.draw2D.addVertex(float2(0, 0), float2(0.0, 1.0), c) 36 | 37 | game.draw2D.addVertex(float2(size.x, size.y), float2(1.0, 0.0), c) 38 | game.draw2D.addVertex(float2(0, 0), float2(0.0, 1.0), c) 39 | game.draw2D.addVertex(float2(size.x, 0), float2(1.0, 1.0), c) 40 | game.draw2D.endShape() 41 | } 42 | } 43 | 44 | /// CreateTexture 45 | @_silgen_name("CreateTexture") 46 | func CreateTexture(width: Int, height: Int) -> Int { 47 | if let game = globalGame { 48 | if let id = game.draw2D.createTexture(width: width, height: height) { 49 | return id 50 | } 51 | } 52 | return -1 53 | } 54 | 55 | /// LoadTexture 56 | @_silgen_name("LoadTexture") 57 | func LoadTexture(name: UnsafePointer) -> Int { 58 | if let name = String(cString: name, encoding: .utf8) { 59 | if let game = globalGame { 60 | if let id = game.draw2D.loadTexture(name) { 61 | return id 62 | } 63 | } 64 | } 65 | return -1 66 | } 67 | 68 | /// SetTarget 69 | @_silgen_name("SetTarget") 70 | @discardableResult func SetTarget(id: Int) -> Bool { 71 | if let game = globalGame { 72 | return game.draw2D.setTarget(id: id) 73 | } 74 | return false 75 | } 76 | 77 | /// SetTexture 78 | @_silgen_name("SetTexture") 79 | @discardableResult func SetTexture(id: Int) -> Bool { 80 | if let game = globalGame { 81 | return game.draw2D.setTexture(id: id) 82 | } 83 | return false 84 | } 85 | 86 | /// SetFont 87 | @_silgen_name("SetFont") 88 | @discardableResult func SetFont(name: UnsafePointer) -> Bool { 89 | if let name = String(cString: name, encoding: .utf8) { 90 | if let game = globalGame { 91 | return game.draw2D.setFont(name: name.lowercased()) 92 | } 93 | } 94 | return false 95 | } 96 | 97 | /// SetFont 98 | @_silgen_name("GetFontSize") 99 | @discardableResult func GetFontSize(text: UnsafePointer, size: Float) -> Vector2 { 100 | if let text = String(cString: text, encoding: .utf8) { 101 | if let game = globalGame { 102 | let rc = game.draw2D.getTextSize(text: text, size: size) 103 | return Vector2(x: rc.x, y: rc.y) 104 | } 105 | } 106 | return Vector2(x: 0, y: 0) 107 | } 108 | 109 | /// DrawText 110 | @_silgen_name("DrawText") 111 | func DrawText(pos: Vector2, text: UnsafePointer, size: Float, color: Color) { 112 | if let game = globalGame { 113 | if let text = String(cString: text, encoding: .utf8) { 114 | let c = colorToFloat4(color) 115 | 116 | //game.draw2D.startShape(type: .triangle) 117 | game.draw2D.drawText(position: float2(pos.x, pos.y), text: text, size: size, color: c) 118 | //game.draw2D.endShape() 119 | } 120 | } 121 | } 122 | 123 | /// DrawRect 124 | @_silgen_name("DrawRect") 125 | func DrawRect(rect: Rectangle, color: Color) { 126 | if let game = globalGame { 127 | let c = colorToFloat4(color) 128 | game.draw2D.startShape(type: .triangle) 129 | game.draw2D.drawRect(rect, c, 0.0) 130 | game.draw2D.endShape() 131 | } 132 | } 133 | 134 | /// DrawRectRotCenter 135 | @_silgen_name("DrawRectRotCenter") 136 | func DrawRectRotCenter(rect: Rectangle, color: Color, rot: Float) { 137 | if let game = globalGame { 138 | let c = colorToFloat4(color) 139 | game.draw2D.startShape(type: .triangle) 140 | game.draw2D.drawRect(rect, c, rot) 141 | game.draw2D.endShape() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /metalRay Shared/Misc/Font.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Font.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import MetalKit 9 | 10 | struct BMChar : Decodable { 11 | let id : Int 12 | let index : Int 13 | let char : String 14 | let width : Float 15 | let height : Float 16 | let xoffset : Float 17 | let yoffset : Float 18 | let xadvance : Float 19 | let chnl : Int 20 | let x : Float 21 | let y : Float 22 | let page : Int 23 | } 24 | 25 | struct BMCommon : Decodable { 26 | let lineHeight : Float 27 | } 28 | 29 | struct BMFont : Decodable { 30 | let pages : [String] 31 | let chars : [BMChar] 32 | let common : BMCommon 33 | } 34 | 35 | class Font 36 | { 37 | var uuid = UUID() 38 | 39 | var name : String 40 | var game : Game 41 | 42 | var atlas : MTLTexture? 43 | var bmFont : BMFont? 44 | 45 | init(name: String, game: Game) 46 | { 47 | self.name = name 48 | self.game = game 49 | 50 | atlas = loadTexture( name ) 51 | 52 | let path = Bundle.main.path(forResource: name, ofType: "json")! 53 | let data = NSData(contentsOfFile: path)! as Data 54 | 55 | guard let font = try? JSONDecoder().decode(BMFont.self, from: data) else { 56 | print("Error: Could not decode JSON of \(name)") 57 | return 58 | } 59 | bmFont = font 60 | } 61 | 62 | deinit { 63 | if let texture = atlas { 64 | texture.setPurgeableState(.empty) 65 | atlas = nil 66 | bmFont = nil 67 | } 68 | } 69 | 70 | /* 71 | func createTextBuffer(_ object: [AnyHashable:Any]) -> TextBuffer 72 | { 73 | /* 74 | //var x : Float; if let v = object["x"] as? Float { x = v } else { x = 0 } 75 | //var y : Float; if let v = object["y"] as? Float { y = v } else { y = 0 } 76 | let size : Float; if let v = object["size"] as? Float { size = v } else { size = 1 } 77 | let text : String; if let v = object["text"] as? String { text = v } else { text = "" } 78 | 79 | var array : [CharBuffer] = [] 80 | 81 | //if textBuffer != nil { 82 | // print("No buffer for", text, textBuffer, textBuffer!.x, x, textBuffer!.y, y) 83 | //} 84 | 85 | for c in text { 86 | let bmChar = getItemForChar( c ) 87 | if bmChar != nil { 88 | //let char = drawChar( font, char: bmChar!, x: posX + bmChar!.xoffset * adjScale, y: y + bmChar!.yoffset * adjScale, color: color, scale: scale, fragment: fragment) 89 | array.append(char) 90 | //print( bmChar?.char, bmChar?.x, bmChar?.y, bmChar?.width, bmChar?.height) 91 | posX += bmChar!.xadvance * adjScale; 92 | 93 | } 94 | } 95 | */ 96 | 97 | return TextBuffer(chars:array, x: x, y: y, viewWidth: mmRenderer.width, viewHeight: mmRenderer.height) 98 | }*/ 99 | 100 | func loadTexture(_ name: String, mipmaps: Bool = false, sRGB: Bool = false ) -> MTLTexture? 101 | { 102 | let path = Bundle.main.path(forResource: name, ofType: "tiff")! 103 | let data = NSData(contentsOfFile: path)! as Data 104 | 105 | let options: [MTKTextureLoader.Option : Any] = [.generateMipmaps : mipmaps, .SRGB : sRGB] 106 | 107 | return try? game.textureLoader.newTexture(data: data, options: options) 108 | } 109 | 110 | func getLineHeight(_ fontScale: Float) -> Float 111 | { 112 | return (bmFont!.common.lineHeight * fontScale) / 2 113 | } 114 | 115 | func getItemForChar(_ char: Character ) -> BMChar? 116 | { 117 | let array = bmFont!.chars 118 | 119 | for item in array { 120 | if Character( item.char ) == char { 121 | return item 122 | } 123 | } 124 | return nil 125 | } 126 | 127 | @discardableResult func getTextRect(text: String, scale: Float = 1.0, rectToUse: MRRect? = nil) -> MRRect 128 | { 129 | var rect : MRRect 130 | if rectToUse == nil { 131 | rect = MRRect() 132 | } else { 133 | rect = rectToUse! 134 | } 135 | 136 | rect.width = 0 137 | rect.height = 0 138 | 139 | for c in text { 140 | let bmChar = getItemForChar( c ) 141 | if bmChar != nil { 142 | rect.width += bmChar!.xadvance * scale / 2; 143 | rect.height = max( rect.height, (bmChar!.height /*- bmChar!.yoffset*/) * scale / 2) 144 | } 145 | } 146 | 147 | return rect; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /metalRay Shared/Draw/MetalStates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalStates.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import MetalKit 9 | 10 | class MetalStates { 11 | 12 | enum States : Int { 13 | case DrawDisc, CopyTexture, DrawTexture, DrawBox, DrawBoxExt, DrawTextChar, DrawBackPattern, DrawLine, DrawBezier, DrawGrid, RenderVoxelTiles 14 | } 15 | 16 | enum ComputeStates : Int { 17 | case MakeCGIImage 18 | } 19 | 20 | var defaultLibrary : MTLLibrary! 21 | 22 | let pipelineStateDescriptor : MTLRenderPipelineDescriptor 23 | 24 | var states : [Int:MTLRenderPipelineState] = [:] 25 | var computeStates : [Int:MTLComputePipelineState] = [:] 26 | 27 | var rayView : RayView 28 | 29 | init(_ rayView: RayView) 30 | { 31 | self.rayView = rayView 32 | 33 | defaultLibrary = rayView.device!.makeDefaultLibrary() 34 | 35 | let vertexFunction = defaultLibrary!.makeFunction( name: "m4mQuadVertexShader" ) 36 | 37 | pipelineStateDescriptor = MTLRenderPipelineDescriptor() 38 | pipelineStateDescriptor.vertexFunction = vertexFunction 39 | // pipelineStateDescriptor.fragmentFunction = fragmentFunction 40 | pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unorm; 41 | 42 | pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true 43 | pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add 44 | pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add 45 | pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha 46 | pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha 47 | pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha 48 | pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha 49 | 50 | // states[States.DrawDisc.rawValue] = createQuadState(name: "m4mDiscDrawable") 51 | // states[States.CopyTexture.rawValue] = createQuadState(name: "m4mCopyTextureDrawable") 52 | // states[States.DrawTexture.rawValue] = createQuadState(name: "m4mTextureDrawable") 53 | // states[States.DrawBox.rawValue] = createQuadState(name: "m4mBoxDrawable") 54 | // states[States.DrawBoxExt.rawValue] = createQuadState(name: "m4mBoxDrawableExt") 55 | // states[States.DrawTextChar.rawValue] = createQuadState(name: "m4mTextDrawable") 56 | // states[States.DrawBackPattern.rawValue] = createQuadState(name: "m4mBoxPatternDrawable") 57 | // states[States.DrawLine.rawValue] = createQuadState(name: "m4mLineDrawable") 58 | // states[States.DrawBezier.rawValue] = createQuadState(name: "m4mBezierDrawable") 59 | // states[States.DrawGrid.rawValue] = createQuadState(name: "m4mGridDrawable") 60 | // states[States.RenderVoxelTiles.rawValue] = createQuadState(name: "renderVoxelTiles") 61 | 62 | computeStates[ComputeStates.MakeCGIImage.rawValue] = createComputeState(name: "makeCGIImage") 63 | } 64 | 65 | /// Creates a quad state from an optional library and the function name 66 | func createQuadState( library: MTLLibrary? = nil, name: String ) -> MTLRenderPipelineState? 67 | { 68 | let function : MTLFunction? 69 | 70 | if library != nil { 71 | function = library!.makeFunction( name: name ) 72 | } else { 73 | function = defaultLibrary!.makeFunction( name: name ) 74 | } 75 | 76 | var renderPipelineState : MTLRenderPipelineState? 77 | 78 | do { 79 | //renderPipelineState = try rayView.device?.makeComputePipelineState( function: function! ) 80 | pipelineStateDescriptor.fragmentFunction = function 81 | renderPipelineState = try rayView.device?.makeRenderPipelineState( descriptor: pipelineStateDescriptor ) 82 | } catch { 83 | print( "pipelineState failed" ) 84 | return nil 85 | } 86 | 87 | return renderPipelineState 88 | } 89 | 90 | /// Creates a compute state from an optional library and the function name 91 | func createComputeState( library: MTLLibrary? = nil, name: String ) -> MTLComputePipelineState? 92 | { 93 | let function : MTLFunction? 94 | 95 | if library != nil { 96 | function = library!.makeFunction( name: name ) 97 | } else { 98 | function = defaultLibrary!.makeFunction( name: name ) 99 | } 100 | 101 | var computePipelineState : MTLComputePipelineState? 102 | 103 | if function == nil { 104 | return nil 105 | } 106 | 107 | do { 108 | computePipelineState = try rayView.device!.makeComputePipelineState( function: function! ) 109 | } catch { 110 | print( "computePipelineState failed" ) 111 | return nil 112 | } 113 | 114 | return computePipelineState 115 | } 116 | 117 | func getState(state: States) -> MTLRenderPipelineState 118 | { 119 | return states[state.rawValue]! 120 | } 121 | 122 | func getComputeState(state: ComputeStates) -> MTLComputePipelineState 123 | { 124 | return computeStates[state.rawValue]! 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## metalRay 3 | 4 | **metalRay** is a bare bones game framework for the Apple ecosystem. If you want to code games close to the Metal with a convenient API (in the tradition of frameworks like [raylib](https://raylib.com)) you will feel right at home. 5 | 6 | You can write games in Swift and in C directly in Xcode while being able to create C style interoperable memory structures and pass them to your Metal shaders. 7 | 8 | *metalRay* focuses right now on 2D drawing, 3D support will be integrated once 2D is stable. 9 | 10 | ## Features 11 | 12 | * Use drawing functions, textures and shaders with an easy to use API. 13 | * Share memory (C style structs) between Swift, C and Metal. 14 | * System device events can be easily queried in the game update functions. 15 | * Draw text using SDF textures. 16 | * Support for 2D physics and Tiled are incoming. 17 | * Games / Apps can be deployed easily to macOS, iOS and tvOS using Xcode. 18 | 19 | ## Why ? 20 | 21 | I like to write games low level, and all the popular convenience frameworks out there (SDL2, raylib etc.) are not based on Metal but OpenGL, which makes iOS and tvOS compatibility problematic. 22 | 23 | And being nostalgic, I also really enjoy coding in C again sometimes. Especially for implementing some of the classics. Get your hands dirty! 24 | 25 | Being able to deploy your games easily to macOS, iOS and tvOS is a major advantage compared to the mostly limited cross-platform alternatives. 26 | 27 | #### Downsides 28 | 29 | * The API is implemented in Swift, however with a C style calling convention to make it work in both Swift and C. So no swiftiniess and functions return -1 if something goes wrong (and not null). Texts are passed as C Strings (use *toCStr()* to convert Strings to C strings). 30 | * While it is super convenient to code your game directly in Xcode, the downside is that merging your fork of this repo can be a bit tedious (when updated). Basically keep your files limited to the *Game* folder and merge everything outside the *Game* folder (Swift, C, Metal, .h) files. 31 | 32 | ## How to use 33 | 34 | Fork this repository and open the Xcode project. All game related functions are inside the **Game** folder 35 | 36 | * **game.swift**. The Swift based game entry point. Use the initGame(), updateGame() and deinitGame() functons. 37 | 38 | * **game.c** The corresponding C file with InitGame(), UpdateGame() and DeinitGame(). 39 | 40 | * **header.h**. Implement C like structures here. These structures can be used in Swift and C as well as in Metal. 41 | 42 | By default the Swift functions are called. If you want to implement (or mix) with the C code, call the C functions from there. See the example default code. 43 | 44 | Place all your resources somewhere in the *Game* folder. 45 | 46 | The Xcode project contains targets for macOS, iOS and tvOS. 47 | 48 | I will soon add some Swift and C examples to the project. 49 | 50 | ## Status 51 | 52 | - [x] Render Targets 53 | - [x] Textures 54 | - [x] Rectangle Drawing 55 | - [x] SDF Text Drawing 56 | - [x] Input Events Partially implemented (macOS, iOS, tvOS) 57 | - [ ] SDF based Shapes 58 | - [ ] Custom Shaders 59 | - [ ] 2D Physics 60 | - [ ] Tiled Import 61 | - [ ] 3D Support 62 | 63 | --- 64 | 65 | ## Structures used in the API 66 | 67 | Loaned from raylib. 68 | 69 | ```c 70 | // Color, 4 components, R8G8B8A8 (32bit) 71 | typedef struct Color { 72 | unsigned char r; // Color red value 73 | unsigned char g; // Color green value 74 | unsigned char b; // Color blue value 75 | unsigned char a; // Color alpha value 76 | } Color; 77 | 78 | // Vector2, 2 components 79 | typedef struct Vector2 { 80 | float x; // Vector x component 81 | float y; // Vector y component 82 | } Vector2; 83 | 84 | // Vector3, 3 components 85 | typedef struct Vector3 { 86 | float x; // Vector x component 87 | float y; // Vector y component 88 | float z; // Vector z component 89 | } Vector3; 90 | 91 | // Vector4, 4 components 92 | typedef struct Vector4 { 93 | float x; // Vector x component 94 | float y; // Vector y component 95 | float z; // Vector z component 96 | float w; // Vector w component 97 | } Vector4; 98 | 99 | // Rectangle, 4 components 100 | typedef struct Rectangle { 101 | float x; // Rectangle top-left corner position x 102 | float y; // Rectangle top-left corner position y 103 | float width; // Rectangle width 104 | float height; // Rectangle height 105 | } Rectangle; 106 | ``` 107 | 108 | These structures are used in the API and can be created from both Swift and C and passed to Metal shaders. 109 | 110 | --- 111 | 112 | ## Window / Events 113 | 114 | ```swift 115 | // Returns the size of the screen / device 116 | GetScreenSize() -> Vector2; 117 | 118 | // Left mouse click or touch event 119 | HasTap() -> Bool; 120 | 121 | // Left mouse double click or double touch event 122 | HasDoubleTap() -> Bool; 123 | 124 | // Left mouse is down or ongoing touch event 125 | HasTouch() -> Bool; 126 | 127 | // Left mouse up or touch up event 128 | HasTouchEnded() -> Bool; 129 | 130 | // Current mouse or touch event position in window / device 131 | GetTouchPos() -> Vector2; 132 | ``` 133 | 134 | ## Drawing 135 | 136 | ```swift 137 | // Starts drawing, if you change the render target you need to end drawing to your current target first. 138 | BeginDrawing(); 139 | // End drawing 140 | EndDrawing(); 141 | 142 | // Sets the current font 143 | SetFont(name: CString); 144 | // Returns the size of the given text 145 | GetTextSize(text: CString, size: Float) -> Vector2 146 | // Draws the text of the given size at the given position 147 | DrawText(pos: Vector2, text: CString, size: Float, color: Color); 148 | 149 | // Clears the current render target in the given color 150 | Clear(color: Color); 151 | // Draws a rectangle of the given color 152 | DrawRect(rect: Rectangle, color: Color); 153 | // Draws a rotated (around its center) rectangle of the given color 154 | DrawRectRotCenter(rect: Rectangle, color: Color, rot: Int); 155 | // More to come 156 | ``` 157 | 158 | ## Textures 159 | 160 | ```swift 161 | // Creates an RGBA8 texture of the given width and height and returns its id. Returns -1 if unsuccessful. 162 | CreateTexture(width: Int, height: Int) -> Int; 163 | // Load an image in the Xcode project into a texture and returns its id. Returns -1 if unsuccessful. 164 | LoadTexture(name: CString) -> Int; 165 | 166 | // Makes the texture of the given id the new render target. Use 0 to switch back to the default viewport. Make sure to end and restart drawing. 167 | SetTarget(id: Int) -> Bool; 168 | 169 | // Makes the texture of the given id the new texture. All drawing functions will replace the color value with the interpolated texture value. Use 0 to disable texture support (needs to be called before EndDrawing() too). 170 | SetTexture(id: Int) -> Bool; 171 | ``` 172 | -------------------------------------------------------------------------------- /metalRay Shared/RayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RayMTKView.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import SwiftUI 9 | import MetalKit 10 | 11 | public class RayView : MTKView 12 | { 13 | var game : Game! 14 | 15 | var keysDown : [Float] = [] 16 | 17 | var mouseIsDown : Bool = false 18 | var mousePos = float2(0, 0) 19 | 20 | var hasTap : Bool = false 21 | var hasDoubleTap : Bool = false 22 | var hasTouchEnded : Bool = false 23 | 24 | var buttonDown : String? = nil 25 | var swipeDirection : String? = nil 26 | 27 | func reset() 28 | { 29 | keysDown = [] 30 | mouseIsDown = false 31 | hasTap = false 32 | hasDoubleTap = false 33 | buttonDown = nil 34 | swipeDirection = nil 35 | } 36 | 37 | func updated() 38 | { 39 | hasTap = false 40 | hasDoubleTap = false 41 | hasTouchEnded = false 42 | } 43 | 44 | #if os(OSX) 45 | 46 | // --- Key States 47 | var shiftIsDown : Bool = false 48 | var commandIsDown : Bool = false 49 | 50 | override public var acceptsFirstResponder: Bool { return true } 51 | 52 | func platformInit() { 53 | } 54 | 55 | /// To get continuous mouse events on macOS 56 | override public func updateTrackingAreas() 57 | { 58 | let options : NSTrackingArea.Options = [.mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow] 59 | let trackingArea = NSTrackingArea(rect: self.bounds, options: options, 60 | owner: self, userInfo: nil) 61 | self.addTrackingArea(trackingArea) 62 | } 63 | 64 | func setMousePos(_ event: NSEvent) 65 | { 66 | var location = event.locationInWindow 67 | location.y = location.y - CGFloat(frame.height) 68 | location = convert(location, from: nil) 69 | 70 | mousePos.x = Float(location.x) 71 | mousePos.y = -Float(location.y) 72 | } 73 | 74 | override public func keyDown(with event: NSEvent) 75 | { 76 | keysDown.append(Float(event.keyCode)) 77 | } 78 | 79 | override public func keyUp(with event: NSEvent) 80 | { 81 | keysDown.removeAll{$0 == Float(event.keyCode)} 82 | } 83 | 84 | override public func mouseDown(with event: NSEvent) { 85 | if event.clickCount == 2 { 86 | hasDoubleTap = true 87 | } else { 88 | hasTap = true 89 | mouseIsDown = true 90 | setMousePos(event) 91 | } 92 | } 93 | 94 | override public func mouseMoved(with event: NSEvent) { 95 | setMousePos(event) 96 | } 97 | 98 | override public func mouseDragged(with event: NSEvent) { 99 | setMousePos(event) 100 | } 101 | 102 | override public func mouseUp(with event: NSEvent) { 103 | mouseIsDown = false 104 | hasTouchEnded = true 105 | setMousePos(event) 106 | } 107 | 108 | override public func flagsChanged(with event: NSEvent) { 109 | //https://stackoverflow.com/questions/9268045/how-can-i-detect-that-the-shift-key-has-been-pressed 110 | if game.state == .Idle { 111 | if event.modifierFlags.contains(.shift) { 112 | shiftIsDown = true 113 | } else { 114 | shiftIsDown = false 115 | } 116 | if event.modifierFlags.contains(.command) { 117 | commandIsDown = true 118 | } else { 119 | commandIsDown = false 120 | } 121 | } 122 | } 123 | 124 | override public func scrollWheel(with event: NSEvent) { 125 | if game.state == .Idle { 126 | //game.mapBuilder.mapPreview.scrollWheel(with: event) 127 | } 128 | } 129 | #elseif os(iOS) 130 | 131 | func platformInit() 132 | { 133 | let tapRecognizer = UITapGestureRecognizer(target: self, action:(#selector(self.handleTapGesture(_:)))) 134 | tapRecognizer.numberOfTapsRequired = 1 135 | addGestureRecognizer(tapRecognizer) 136 | 137 | let pinchRecognizer = UIPinchGestureRecognizer(target: self, action:(#selector(self.handlePinchGesture(_:)))) 138 | addGestureRecognizer(pinchRecognizer) 139 | } 140 | 141 | var lastPinch : Float = 1 142 | 143 | @objc func handlePinchGesture(_ recognizer: UIPinchGestureRecognizer) 144 | { 145 | /* 146 | if game.state == .Idle { 147 | if let asset = game.assetFolder.current, asset.type == .Map { 148 | if let map = asset.map { 149 | let pinch = Float(recognizer.scale) 150 | if pinch >= lastPinch { 151 | map.camera2D.zoom += pinch * 0.2 152 | } else { 153 | map.camera2D.zoom -= pinch * 0.2 154 | } 155 | lastPinch = pinch 156 | map.camera2D.zoom = max(map.camera2D.zoom, 0.01) 157 | game.mapBuilder.createPreview(map, true) 158 | } 159 | } 160 | }*/ 161 | } 162 | 163 | @objc func handleTapGesture(_ recognizer: UITapGestureRecognizer) 164 | { 165 | if recognizer.numberOfTouches == 1 { 166 | hasTap = true 167 | // DispatchQueue.main.asyncAfter(deadline: .now() + 1.0 / 60.0) { 168 | // self.hasTap = false 169 | // } 170 | } else 171 | if recognizer.numberOfTouches >= 1 { 172 | hasDoubleTap = true 173 | // DispatchQueue.main.asyncAfter(deadline: .now() + 1.0 / 60.0) { 174 | // self.hasDoubleTap = false 175 | // } 176 | } 177 | } 178 | 179 | func setMousePos(_ x: Float, _ y: Float) 180 | { 181 | mousePos.x = x 182 | mousePos.y = y 183 | 184 | //mousePos.x /= Float(bounds.width) / game.texture!.width// / game.scaleFactor 185 | //mousePos.y /= Float(bounds.height) / game.texture!.height// / game.scaleFactor 186 | } 187 | 188 | var firstTouch = float2(0,0) 189 | override public func touchesBegan(_ touches: Set, with event: UIEvent?) { 190 | 191 | mouseIsDown = true 192 | if let touch = touches.first { 193 | let point = touch.location(in: self) 194 | setMousePos(Float(point.x), Float(point.y)) 195 | 196 | firstTouch.x = mousePos.x 197 | firstTouch.y = mousePos.y 198 | } 199 | } 200 | 201 | override public func touchesMoved(_ touches: Set, with event: UIEvent?) { 202 | if let touch = touches.first { 203 | let point = touch.location(in: self) 204 | setMousePos(Float(point.x), Float(point.y)) 205 | 206 | firstTouch.x = mousePos.x 207 | firstTouch.y = mousePos.y 208 | } 209 | } 210 | 211 | override public func touchesEnded(_ touches: Set, with event: UIEvent?) { 212 | mouseIsDown = false 213 | hasTouchEnded = true 214 | if let touch = touches.first { 215 | let point = touch.location(in: self) 216 | setMousePos(Float(point.x), Float(point.y)) 217 | } 218 | } 219 | 220 | #elseif os(tvOS) 221 | 222 | func platformInit() 223 | { 224 | var swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedRight)) 225 | swipeRecognizer.direction = .right 226 | addGestureRecognizer(swipeRecognizer) 227 | 228 | swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedLeft)) 229 | swipeRecognizer.direction = .left 230 | addGestureRecognizer(swipeRecognizer) 231 | 232 | swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedUp)) 233 | swipeRecognizer.direction = .up 234 | addGestureRecognizer(swipeRecognizer) 235 | 236 | swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedDown)) 237 | swipeRecognizer.direction = .down 238 | addGestureRecognizer(swipeRecognizer) 239 | } 240 | 241 | public override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) 242 | { 243 | guard let buttonPress = presses.first?.type else { return } 244 | 245 | switch(buttonPress) { 246 | case .menu: 247 | buttonDown = "Menu" 248 | case .playPause: 249 | buttonDown = "Play/Pause" 250 | case .select: 251 | buttonDown = "Select" 252 | case .upArrow: 253 | buttonDown = "ArrowUp" 254 | case .downArrow: 255 | buttonDown = "ArrowDown" 256 | case .leftArrow: 257 | buttonDown = "ArrowLeft" 258 | case .rightArrow: 259 | buttonDown = "ArrowRight" 260 | default: 261 | print("Unkown Button", buttonPress) 262 | } 263 | } 264 | 265 | public override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) 266 | { 267 | buttonDown = nil 268 | } 269 | 270 | @objc func swipedUp() { 271 | swipeDirection = "up" 272 | } 273 | 274 | @objc func swipedDown() { 275 | swipeDirection = "down" 276 | } 277 | 278 | @objc func swipedRight() { 279 | swipeDirection = "right" 280 | } 281 | 282 | @objc func swipedLeft() { 283 | swipeDirection = "left" 284 | } 285 | 286 | 287 | #endif 288 | } 289 | -------------------------------------------------------------------------------- /metalRay Shared/Fonts/OpenSans.json: -------------------------------------------------------------------------------- 1 | {"pages":["OpenSans-Regular.png"],"chars":[{"id":124,"index":95,"char":"|","width":14,"height":92,"xoffset":16,"yoffset":4,"xadvance":46,"chnl":15,"x":0,"y":0,"page":0},{"id":106,"index":77,"char":"j","width":27,"height":90,"xoffset":-9,"yoffset":6,"xadvance":21,"chnl":15,"x":15,"y":0,"page":0},{"id":74,"index":45,"char":"J","width":29,"height":84,"xoffset":-11,"yoffset":8,"xadvance":22,"chnl":15,"x":43,"y":0,"page":0},{"id":87,"index":58,"char":"W","width":84,"height":68,"xoffset":-3,"yoffset":8,"xadvance":78,"chnl":15,"x":73,"y":0,"page":0},{"id":81,"index":52,"char":"Q","width":63,"height":83,"xoffset":1,"yoffset":7,"xadvance":65,"chnl":15,"x":73,"y":69,"page":0},{"id":93,"index":64,"char":"]","width":27,"height":81,"xoffset":-2,"yoffset":8,"xadvance":28,"chnl":15,"x":43,"y":85,"page":0},{"id":123,"index":94,"char":"{","width":34,"height":81,"xoffset":-1,"yoffset":8,"xadvance":32,"chnl":15,"x":0,"y":93,"page":0},{"id":125,"index":96,"char":"}","width":34,"height":81,"xoffset":-1,"yoffset":8,"xadvance":32,"chnl":15,"x":137,"y":69,"page":0},{"id":40,"index":11,"char":"(","width":27,"height":81,"xoffset":-1,"yoffset":8,"xadvance":25,"chnl":15,"x":137,"y":151,"page":0},{"id":91,"index":62,"char":"[","width":27,"height":81,"xoffset":3,"yoffset":8,"xadvance":28,"chnl":15,"x":165,"y":151,"page":0},{"id":41,"index":12,"char":")","width":27,"height":81,"xoffset":-1,"yoffset":8,"xadvance":25,"chnl":15,"x":71,"y":153,"page":0},{"id":36,"index":7,"char":"$","width":45,"height":77,"xoffset":1,"yoffset":4,"xadvance":48,"chnl":15,"x":0,"y":175,"page":0},{"id":64,"index":35,"char":"@","width":74,"height":76,"xoffset":1,"yoffset":8,"xadvance":76,"chnl":15,"x":172,"y":0,"page":0},{"id":103,"index":74,"char":"g","width":50,"height":74,"xoffset":-2,"yoffset":22,"xadvance":46,"chnl":15,"x":193,"y":77,"page":0},{"id":112,"index":83,"char":"p","width":48,"height":74,"xoffset":3,"yoffset":22,"xadvance":51,"chnl":15,"x":193,"y":152,"page":0},{"id":113,"index":84,"char":"q","width":48,"height":74,"xoffset":1,"yoffset":22,"xadvance":51,"chnl":15,"x":193,"y":227,"page":0},{"id":121,"index":92,"char":"y","width":50,"height":73,"xoffset":-4,"yoffset":23,"xadvance":42,"chnl":15,"x":99,"y":233,"page":0},{"id":98,"index":69,"char":"b","width":48,"height":73,"xoffset":3,"yoffset":4,"xadvance":51,"chnl":15,"x":46,"y":235,"page":0},{"id":100,"index":71,"char":"d","width":48,"height":73,"xoffset":1,"yoffset":4,"xadvance":51,"chnl":15,"x":242,"y":152,"page":0},{"id":108,"index":79,"char":"l","width":15,"height":72,"xoffset":3,"yoffset":4,"xadvance":21,"chnl":15,"x":172,"y":77,"page":0},{"id":109,"index":80,"char":"m","width":72,"height":54,"xoffset":3,"yoffset":22,"xadvance":78,"chnl":15,"x":244,"y":77,"page":0},{"id":107,"index":78,"char":"k","width":44,"height":72,"xoffset":3,"yoffset":4,"xadvance":44,"chnl":15,"x":0,"y":253,"page":0},{"id":102,"index":73,"char":"f","width":39,"height":72,"xoffset":-3,"yoffset":4,"xadvance":28,"chnl":15,"x":150,"y":233,"page":0},{"id":104,"index":75,"char":"h","width":46,"height":72,"xoffset":3,"yoffset":4,"xadvance":52,"chnl":15,"x":247,"y":0,"page":0},{"id":119,"index":90,"char":"w","width":71,"height":53,"xoffset":-3,"yoffset":23,"xadvance":65,"chnl":15,"x":242,"y":226,"page":0},{"id":67,"index":38,"char":"C","width":53,"height":70,"xoffset":1,"yoffset":7,"xadvance":53,"chnl":15,"x":294,"y":0,"page":0},{"id":71,"index":42,"char":"G","width":58,"height":70,"xoffset":1,"yoffset":7,"xadvance":61,"chnl":15,"x":291,"y":132,"page":0},{"id":56,"index":27,"char":"8","width":47,"height":70,"xoffset":0,"yoffset":7,"xadvance":48,"chnl":15,"x":314,"y":203,"page":0},{"id":54,"index":25,"char":"6","width":47,"height":70,"xoffset":1,"yoffset":7,"xadvance":48,"chnl":15,"x":314,"y":274,"page":0},{"id":37,"index":8,"char":"%","width":69,"height":70,"xoffset":0,"yoffset":7,"xadvance":69,"chnl":15,"x":242,"y":280,"page":0},{"id":79,"index":50,"char":"O","width":63,"height":70,"xoffset":1,"yoffset":7,"xadvance":65,"chnl":15,"x":150,"y":306,"page":0},{"id":83,"index":54,"char":"S","width":46,"height":70,"xoffset":0,"yoffset":7,"xadvance":46,"chnl":15,"x":95,"y":307,"page":0},{"id":105,"index":76,"char":"i","width":16,"height":70,"xoffset":3,"yoffset":6,"xadvance":21,"chnl":15,"x":214,"y":302,"page":0},{"id":51,"index":22,"char":"3","width":47,"height":70,"xoffset":0,"yoffset":7,"xadvance":48,"chnl":15,"x":45,"y":309,"page":0},{"id":38,"index":9,"char":"&","width":65,"height":70,"xoffset":1,"yoffset":7,"xadvance":61,"chnl":15,"x":348,"y":0,"page":0},{"id":48,"index":19,"char":"0","width":48,"height":70,"xoffset":0,"yoffset":7,"xadvance":48,"chnl":15,"x":350,"y":71,"page":0},{"id":63,"index":34,"char":"?","width":41,"height":70,"xoffset":-3,"yoffset":7,"xadvance":36,"chnl":15,"x":0,"y":326,"page":0},{"id":57,"index":28,"char":"9","width":47,"height":70,"xoffset":0,"yoffset":7,"xadvance":48,"chnl":15,"x":362,"y":142,"page":0},{"id":50,"index":21,"char":"2","width":47,"height":69,"xoffset":0,"yoffset":7,"xadvance":48,"chnl":15,"x":362,"y":213,"page":0},{"id":85,"index":56,"char":"U","width":54,"height":69,"xoffset":4,"yoffset":8,"xadvance":61,"chnl":15,"x":362,"y":283,"page":0},{"id":33,"index":4,"char":"!","width":18,"height":69,"xoffset":2,"yoffset":8,"xadvance":22,"chnl":15,"x":399,"y":71,"page":0},{"id":53,"index":24,"char":"5","width":46,"height":69,"xoffset":1,"yoffset":8,"xadvance":48,"chnl":15,"x":312,"y":345,"page":0},{"id":35,"index":6,"char":"#","width":58,"height":68,"xoffset":-2,"yoffset":8,"xadvance":54,"chnl":15,"x":359,"y":353,"page":0},{"id":77,"index":48,"char":"M","width":67,"height":68,"xoffset":4,"yoffset":8,"xadvance":76,"chnl":15,"x":214,"y":373,"page":0},{"id":78,"index":49,"char":"N","width":55,"height":68,"xoffset":4,"yoffset":8,"xadvance":63,"chnl":15,"x":142,"y":377,"page":0},{"id":47,"index":18,"char":"/","width":37,"height":68,"xoffset":-3,"yoffset":8,"xadvance":31,"chnl":15,"x":99,"y":153,"page":0},{"id":80,"index":51,"char":"P","width":46,"height":68,"xoffset":4,"yoffset":8,"xadvance":51,"chnl":15,"x":93,"y":378,"page":0},{"id":52,"index":23,"char":"4","width":53,"height":68,"xoffset":-2,"yoffset":8,"xadvance":48,"chnl":15,"x":0,"y":397,"page":0},{"id":82,"index":53,"char":"R","width":50,"height":68,"xoffset":4,"yoffset":8,"xadvance":52,"chnl":15,"x":414,"y":0,"page":0},{"id":65,"index":36,"char":"A","width":61,"height":68,"xoffset":-4,"yoffset":8,"xadvance":53,"chnl":15,"x":410,"y":141,"page":0},{"id":84,"index":55,"char":"T","width":53,"height":68,"xoffset":-3,"yoffset":8,"xadvance":46,"chnl":15,"x":418,"y":69,"page":0},{"id":66,"index":37,"char":"B","width":50,"height":68,"xoffset":4,"yoffset":8,"xadvance":54,"chnl":15,"x":410,"y":210,"page":0},{"id":86,"index":57,"char":"V","width":58,"height":68,"xoffset":-4,"yoffset":8,"xadvance":50,"chnl":15,"x":417,"y":279,"page":0},{"id":76,"index":47,"char":"L","width":41,"height":68,"xoffset":4,"yoffset":8,"xadvance":44,"chnl":15,"x":418,"y":348,"page":0},{"id":88,"index":59,"char":"X","width":56,"height":68,"xoffset":-4,"yoffset":8,"xadvance":48,"chnl":15,"x":418,"y":417,"page":0},{"id":89,"index":60,"char":"Y","width":55,"height":68,"xoffset":-4,"yoffset":8,"xadvance":47,"chnl":15,"x":282,"y":415,"page":0},{"id":90,"index":61,"char":"Z","width":49,"height":68,"xoffset":-1,"yoffset":8,"xadvance":48,"chnl":15,"x":198,"y":442,"page":0},{"id":68,"index":39,"char":"D","width":56,"height":68,"xoffset":4,"yoffset":8,"xadvance":61,"chnl":15,"x":140,"y":446,"page":0},{"id":92,"index":63,"char":"\\","width":37,"height":68,"xoffset":-3,"yoffset":8,"xadvance":31,"chnl":15,"x":54,"y":380,"page":0},{"id":69,"index":40,"char":"E","width":41,"height":68,"xoffset":4,"yoffset":8,"xadvance":47,"chnl":15,"x":92,"y":447,"page":0},{"id":70,"index":41,"char":"F","width":41,"height":68,"xoffset":4,"yoffset":8,"xadvance":43,"chnl":15,"x":0,"y":466,"page":0},{"id":49,"index":20,"char":"1","width":30,"height":68,"xoffset":4,"yoffset":8,"xadvance":48,"chnl":15,"x":248,"y":442,"page":0},{"id":72,"index":43,"char":"H","width":54,"height":68,"xoffset":4,"yoffset":8,"xadvance":62,"chnl":15,"x":338,"y":422,"page":0},{"id":73,"index":918,"char":"I","width":15,"height":68,"xoffset":4,"yoffset":8,"xadvance":23,"chnl":15,"x":465,"y":0,"page":0},{"id":55,"index":26,"char":"7","width":48,"height":68,"xoffset":0,"yoffset":8,"xadvance":48,"chnl":15,"x":42,"y":466,"page":0},{"id":75,"index":46,"char":"K","width":51,"height":68,"xoffset":4,"yoffset":8,"xadvance":52,"chnl":15,"x":461,"y":210,"page":0},{"id":59,"index":30,"char":";","width":21,"height":65,"xoffset":-1,"yoffset":22,"xadvance":22,"chnl":15,"x":46,"y":167,"page":0},{"id":116,"index":87,"char":"t","width":35,"height":64,"xoffset":-3,"yoffset":13,"xadvance":30,"chnl":15,"x":460,"y":348,"page":0},{"id":58,"index":29,"char":":","width":18,"height":55,"xoffset":2,"yoffset":22,"xadvance":22,"chnl":15,"x":282,"y":351,"page":0},{"id":99,"index":70,"char":"c","width":40,"height":55,"xoffset":1,"yoffset":22,"xadvance":40,"chnl":15,"x":279,"y":484,"page":0},{"id":115,"index":86,"char":"s","width":40,"height":55,"xoffset":0,"yoffset":22,"xadvance":40,"chnl":15,"x":476,"y":279,"page":0},{"id":111,"index":82,"char":"o","width":49,"height":55,"xoffset":1,"yoffset":22,"xadvance":51,"chnl":15,"x":475,"y":413,"page":0},{"id":101,"index":72,"char":"e","width":46,"height":55,"xoffset":1,"yoffset":22,"xadvance":47,"chnl":15,"x":475,"y":469,"page":0},{"id":97,"index":68,"char":"a","width":44,"height":55,"xoffset":0,"yoffset":22,"xadvance":47,"chnl":15,"x":393,"y":486,"page":0},{"id":110,"index":81,"char":"n","width":46,"height":54,"xoffset":3,"yoffset":22,"xadvance":52,"chnl":15,"x":472,"y":69,"page":0},{"id":114,"index":85,"char":"r","width":34,"height":54,"xoffset":3,"yoffset":22,"xadvance":34,"chnl":15,"x":438,"y":486,"page":0},{"id":117,"index":88,"char":"u","width":46,"height":54,"xoffset":3,"yoffset":23,"xadvance":52,"chnl":15,"x":481,"y":0,"page":0},{"id":120,"index":91,"char":"x","width":49,"height":53,"xoffset":-2,"yoffset":23,"xadvance":44,"chnl":15,"x":472,"y":124,"page":0},{"id":122,"index":93,"char":"z","width":41,"height":53,"xoffset":-1,"yoffset":23,"xadvance":39,"chnl":15,"x":496,"y":335,"page":0},{"id":118,"index":89,"char":"v","width":50,"height":53,"xoffset":-4,"yoffset":23,"xadvance":42,"chnl":15,"x":519,"y":55,"page":0},{"id":60,"index":31,"char":"<","width":47,"height":49,"xoffset":0,"yoffset":17,"xadvance":48,"chnl":15,"x":528,"y":0,"page":0},{"id":62,"index":33,"char":">","width":47,"height":49,"xoffset":0,"yoffset":17,"xadvance":48,"chnl":15,"x":320,"y":491,"page":0},{"id":43,"index":14,"char":"+","width":47,"height":49,"xoffset":0,"yoffset":18,"xadvance":48,"chnl":15,"x":522,"y":469,"page":0},{"id":94,"index":65,"char":"^","width":49,"height":46,"xoffset":-2,"yoffset":8,"xadvance":46,"chnl":15,"x":525,"y":389,"page":0},{"id":42,"index":13,"char":"*","width":47,"height":46,"xoffset":0,"yoffset":4,"xadvance":46,"chnl":15,"x":513,"y":178,"page":0},{"id":126,"index":97,"char":"~","width":47,"height":19,"xoffset":0,"yoffset":33,"xadvance":48,"chnl":15,"x":231,"y":351,"page":0},{"id":95,"index":66,"char":"_","width":46,"height":13,"xoffset":-4,"yoffset":76,"xadvance":38,"chnl":15,"x":244,"y":132,"page":0},{"id":61,"index":32,"char":"=","width":46,"height":30,"xoffset":1,"yoffset":27,"xadvance":48,"chnl":15,"x":525,"y":436,"page":0},{"id":34,"index":5,"char":"\"","width":31,"height":30,"xoffset":1,"yoffset":8,"xadvance":34,"chnl":15,"x":472,"y":178,"page":0},{"id":39,"index":10,"char":"'","width":16,"height":30,"xoffset":1,"yoffset":8,"xadvance":19,"chnl":15,"x":368,"y":491,"page":0},{"id":44,"index":15,"char":",","width":20,"height":29,"xoffset":-1,"yoffset":58,"xadvance":21,"chnl":15,"x":393,"y":422,"page":0},{"id":45,"index":16,"char":"-","width":28,"height":14,"xoffset":-1,"yoffset":42,"xadvance":27,"chnl":15,"x":54,"y":449,"page":0},{"id":96,"index":67,"char":"`","width":24,"height":21,"xoffset":12,"yoffset":4,"xadvance":48,"chnl":15,"x":368,"y":522,"page":0},{"id":46,"index":17,"char":".","width":18,"height":19,"xoffset":2,"yoffset":58,"xadvance":22,"chnl":15,"x":291,"y":203,"page":0},{"id":32,"index":3,"char":" ","width":0,"height":0,"xoffset":-4,"yoffset":68,"xadvance":22,"chnl":15,"x":417,"y":348,"page":0}],"info":{"face":"OpenSans-Regular","size":84,"bold":0,"italic":0,"charset":[" ","!","\"","#","$","%","&","'","(",")","*","+",",","-",".","/",0,1,2,3,4,5,6,7,8,9,":",";","<","=",">","?","@","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","[","\\","]","^","_","`","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","{","|","}","~"],"unicode":1,"stretchH":100,"smooth":1,"aa":1,"padding":[0,0,0,0],"spacing":[0,0]},"common":{"lineHeight":90,"base":68,"scaleW":590,"scaleH":543,"pages":1,"packed":0,"alphaChnl":0,"redChnl":0,"greenChnl":0,"blueChnl":0},"distanceField":{"fieldType":"msdf","distanceRange":8},"kernings":[]} 2 | -------------------------------------------------------------------------------- /metalRay Shared/Game.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Renderer.swift 3 | // metalRay Shared 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | // Our platform independent renderer class 9 | 10 | import Metal 11 | import MetalKit 12 | import simd 13 | 14 | var globalGame : Game? = nil 15 | 16 | class Game: NSObject, MTKViewDelegate { 17 | 18 | enum State { 19 | case Idle, Running, Paused 20 | } 21 | 22 | var state : State = .Idle 23 | 24 | let rayView : RayView 25 | var scaleFactor : Float 26 | var device : MTLDevice! 27 | 28 | var metalStates : MetalStates! 29 | var draw2D : MetalDraw2D! 30 | 31 | var textureLoader : MTKTextureLoader! 32 | 33 | init?(view: RayView) { 34 | rayView = view 35 | 36 | #if os(OSX) 37 | scaleFactor = Float(NSScreen.main!.backingScaleFactor) 38 | #else 39 | scaleFactor = Float(UIScreen.main.scale) 40 | #endif 41 | 42 | super.init() 43 | 44 | textureLoader = MTKTextureLoader(device: rayView.device!) 45 | 46 | rayView.game = self 47 | metalStates = MetalStates(rayView) 48 | draw2D = MetalDraw2D(rayView) 49 | 50 | globalGame = self 51 | 52 | initGame() 53 | } 54 | 55 | func draw(in view: MTKView) { 56 | updateGame() 57 | 58 | rayView.updated() 59 | } 60 | 61 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 62 | 63 | } 64 | } 65 | 66 | /* 67 | 68 | // The 256 byte aligned size of our uniform structure 69 | let alignedUniformsSize = (MemoryLayout.size + 0xFF) & -0x100 70 | 71 | let maxBuffersInFlight = 3 72 | 73 | enum RendererError: Error { 74 | case badVertexDescriptor 75 | } 76 | 77 | class Renderer: NSObject, MTKViewDelegate { 78 | 79 | public let device: MTLDevice 80 | let commandQueue: MTLCommandQueue 81 | var dynamicUniformBuffer: MTLBuffer 82 | var pipelineState: MTLRenderPipelineState 83 | var depthState: MTLDepthStencilState 84 | var colorMap: MTLTexture 85 | 86 | let inFlightSemaphore = DispatchSemaphore(value: maxBuffersInFlight) 87 | 88 | var uniformBufferOffset = 0 89 | 90 | var uniformBufferIndex = 0 91 | 92 | var uniforms: UnsafeMutablePointer 93 | 94 | var projectionMatrix: matrix_float4x4 = matrix_float4x4() 95 | 96 | var rotation: Float = 0 97 | 98 | var mesh: MTKMesh 99 | 100 | init?(metalKitView: MTKView) { 101 | self.device = metalKitView.device! 102 | guard let queue = self.device.makeCommandQueue() else { return nil } 103 | self.commandQueue = queue 104 | 105 | let uniformBufferSize = alignedUniformsSize * maxBuffersInFlight 106 | 107 | guard let buffer = self.device.makeBuffer(length:uniformBufferSize, options:[MTLResourceOptions.storageModeShared]) else { return nil } 108 | dynamicUniformBuffer = buffer 109 | 110 | self.dynamicUniformBuffer.label = "UniformBuffer" 111 | 112 | uniforms = UnsafeMutableRawPointer(dynamicUniformBuffer.contents()).bindMemory(to:Uniforms.self, capacity:1) 113 | 114 | metalKitView.depthStencilPixelFormat = MTLPixelFormat.depth32Float_stencil8 115 | metalKitView.colorPixelFormat = MTLPixelFormat.bgra8Unorm_srgb 116 | metalKitView.sampleCount = 1 117 | 118 | let mtlVertexDescriptor = Renderer.buildMetalVertexDescriptor() 119 | 120 | do { 121 | pipelineState = try Renderer.buildRenderPipelineWithDevice(device: device, 122 | metalKitView: metalKitView, 123 | mtlVertexDescriptor: mtlVertexDescriptor) 124 | } catch { 125 | print("Unable to compile render pipeline state. Error info: \(error)") 126 | return nil 127 | } 128 | 129 | let depthStateDescriptor = MTLDepthStencilDescriptor() 130 | depthStateDescriptor.depthCompareFunction = MTLCompareFunction.less 131 | depthStateDescriptor.isDepthWriteEnabled = true 132 | guard let state = device.makeDepthStencilState(descriptor:depthStateDescriptor) else { return nil } 133 | depthState = state 134 | 135 | do { 136 | mesh = try Renderer.buildMesh(device: device, mtlVertexDescriptor: mtlVertexDescriptor) 137 | } catch { 138 | print("Unable to build MetalKit Mesh. Error info: \(error)") 139 | return nil 140 | } 141 | 142 | do { 143 | colorMap = try Renderer.loadTexture(device: device, textureName: "ColorMap") 144 | } catch { 145 | print("Unable to load texture. Error info: \(error)") 146 | return nil 147 | } 148 | 149 | super.init() 150 | 151 | } 152 | 153 | class func buildMetalVertexDescriptor() -> MTLVertexDescriptor { 154 | // Create a Metal vertex descriptor specifying how vertices will by laid out for input into our render 155 | // pipeline and how we'll layout our Model IO vertices 156 | 157 | let mtlVertexDescriptor = MTLVertexDescriptor() 158 | 159 | mtlVertexDescriptor.attributes[VertexAttribute.position.rawValue].format = MTLVertexFormat.float3 160 | mtlVertexDescriptor.attributes[VertexAttribute.position.rawValue].offset = 0 161 | mtlVertexDescriptor.attributes[VertexAttribute.position.rawValue].bufferIndex = BufferIndex.meshPositions.rawValue 162 | 163 | mtlVertexDescriptor.attributes[VertexAttribute.texcoord.rawValue].format = MTLVertexFormat.float2 164 | mtlVertexDescriptor.attributes[VertexAttribute.texcoord.rawValue].offset = 0 165 | mtlVertexDescriptor.attributes[VertexAttribute.texcoord.rawValue].bufferIndex = BufferIndex.meshGenerics.rawValue 166 | 167 | mtlVertexDescriptor.layouts[BufferIndex.meshPositions.rawValue].stride = 12 168 | mtlVertexDescriptor.layouts[BufferIndex.meshPositions.rawValue].stepRate = 1 169 | mtlVertexDescriptor.layouts[BufferIndex.meshPositions.rawValue].stepFunction = MTLVertexStepFunction.perVertex 170 | 171 | mtlVertexDescriptor.layouts[BufferIndex.meshGenerics.rawValue].stride = 8 172 | mtlVertexDescriptor.layouts[BufferIndex.meshGenerics.rawValue].stepRate = 1 173 | mtlVertexDescriptor.layouts[BufferIndex.meshGenerics.rawValue].stepFunction = MTLVertexStepFunction.perVertex 174 | 175 | return mtlVertexDescriptor 176 | } 177 | 178 | class func buildRenderPipelineWithDevice(device: MTLDevice, 179 | metalKitView: MTKView, 180 | mtlVertexDescriptor: MTLVertexDescriptor) throws -> MTLRenderPipelineState { 181 | /// Build a render state pipeline object 182 | 183 | let library = device.makeDefaultLibrary() 184 | 185 | let vertexFunction = library?.makeFunction(name: "vertexShader") 186 | let fragmentFunction = library?.makeFunction(name: "fragmentShader") 187 | 188 | let pipelineDescriptor = MTLRenderPipelineDescriptor() 189 | pipelineDescriptor.label = "RenderPipeline" 190 | pipelineDescriptor.rasterSampleCount = metalKitView.sampleCount 191 | pipelineDescriptor.vertexFunction = vertexFunction 192 | pipelineDescriptor.fragmentFunction = fragmentFunction 193 | pipelineDescriptor.vertexDescriptor = mtlVertexDescriptor 194 | 195 | pipelineDescriptor.colorAttachments[0].pixelFormat = metalKitView.colorPixelFormat 196 | pipelineDescriptor.depthAttachmentPixelFormat = metalKitView.depthStencilPixelFormat 197 | pipelineDescriptor.stencilAttachmentPixelFormat = metalKitView.depthStencilPixelFormat 198 | 199 | return try device.makeRenderPipelineState(descriptor: pipelineDescriptor) 200 | } 201 | 202 | class func buildMesh(device: MTLDevice, 203 | mtlVertexDescriptor: MTLVertexDescriptor) throws -> MTKMesh { 204 | /// Create and condition mesh data to feed into a pipeline using the given vertex descriptor 205 | 206 | let metalAllocator = MTKMeshBufferAllocator(device: device) 207 | 208 | let mdlMesh = MDLMesh.newBox(withDimensions: SIMD3(4, 4, 4), 209 | segments: SIMD3(2, 2, 2), 210 | geometryType: MDLGeometryType.triangles, 211 | inwardNormals:false, 212 | allocator: metalAllocator) 213 | 214 | let mdlVertexDescriptor = MTKModelIOVertexDescriptorFromMetal(mtlVertexDescriptor) 215 | 216 | guard let attributes = mdlVertexDescriptor.attributes as? [MDLVertexAttribute] else { 217 | throw RendererError.badVertexDescriptor 218 | } 219 | attributes[VertexAttribute.position.rawValue].name = MDLVertexAttributePosition 220 | attributes[VertexAttribute.texcoord.rawValue].name = MDLVertexAttributeTextureCoordinate 221 | 222 | mdlMesh.vertexDescriptor = mdlVertexDescriptor 223 | 224 | return try MTKMesh(mesh:mdlMesh, device:device) 225 | } 226 | 227 | class func loadTexture(device: MTLDevice, 228 | textureName: String) throws -> MTLTexture { 229 | /// Load texture data with optimal parameters for sampling 230 | 231 | let textureLoader = MTKTextureLoader(device: device) 232 | 233 | let textureLoaderOptions = [ 234 | MTKTextureLoader.Option.textureUsage: NSNumber(value: MTLTextureUsage.shaderRead.rawValue), 235 | MTKTextureLoader.Option.textureStorageMode: NSNumber(value: MTLStorageMode.`private`.rawValue) 236 | ] 237 | 238 | return try textureLoader.newTexture(name: textureName, 239 | scaleFactor: 1.0, 240 | bundle: nil, 241 | options: textureLoaderOptions) 242 | 243 | } 244 | 245 | private func updateDynamicBufferState() { 246 | /// Update the state of our uniform buffers before rendering 247 | 248 | uniformBufferIndex = (uniformBufferIndex + 1) % maxBuffersInFlight 249 | 250 | uniformBufferOffset = alignedUniformsSize * uniformBufferIndex 251 | 252 | uniforms = UnsafeMutableRawPointer(dynamicUniformBuffer.contents() + uniformBufferOffset).bindMemory(to:Uniforms.self, capacity:1) 253 | } 254 | 255 | private func updateGameState() { 256 | /// Update any game state before rendering 257 | 258 | uniforms[0].projectionMatrix = projectionMatrix 259 | 260 | let rotationAxis = SIMD3(1, 1, 0) 261 | let modelMatrix = matrix4x4_rotation(radians: rotation, axis: rotationAxis) 262 | let viewMatrix = matrix4x4_translation(0.0, 0.0, -8.0) 263 | uniforms[0].modelViewMatrix = simd_mul(viewMatrix, modelMatrix) 264 | rotation += 0.01 265 | } 266 | 267 | func draw(in view: MTKView) { 268 | /// Per frame updates hare 269 | 270 | _ = inFlightSemaphore.wait(timeout: DispatchTime.distantFuture) 271 | 272 | if let commandBuffer = commandQueue.makeCommandBuffer() { 273 | 274 | let semaphore = inFlightSemaphore 275 | commandBuffer.addCompletedHandler { (_ commandBuffer)-> Swift.Void in 276 | semaphore.signal() 277 | } 278 | 279 | self.updateDynamicBufferState() 280 | 281 | self.updateGameState() 282 | 283 | /// Delay getting the currentRenderPassDescriptor until we absolutely need it to avoid 284 | /// holding onto the drawable and blocking the display pipeline any longer than necessary 285 | let renderPassDescriptor = view.currentRenderPassDescriptor 286 | 287 | if let renderPassDescriptor = renderPassDescriptor, let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) { 288 | 289 | /// Final pass rendering code here 290 | renderEncoder.label = "Primary Render Encoder" 291 | 292 | renderEncoder.pushDebugGroup("Draw Box") 293 | 294 | renderEncoder.setCullMode(.back) 295 | 296 | renderEncoder.setFrontFacing(.counterClockwise) 297 | 298 | renderEncoder.setRenderPipelineState(pipelineState) 299 | 300 | renderEncoder.setDepthStencilState(depthState) 301 | 302 | renderEncoder.setVertexBuffer(dynamicUniformBuffer, offset:uniformBufferOffset, index: BufferIndex.uniforms.rawValue) 303 | renderEncoder.setFragmentBuffer(dynamicUniformBuffer, offset:uniformBufferOffset, index: BufferIndex.uniforms.rawValue) 304 | 305 | for (index, element) in mesh.vertexDescriptor.layouts.enumerated() { 306 | guard let layout = element as? MDLVertexBufferLayout else { 307 | return 308 | } 309 | 310 | if layout.stride != 0 { 311 | let buffer = mesh.vertexBuffers[index] 312 | renderEncoder.setVertexBuffer(buffer.buffer, offset:buffer.offset, index: index) 313 | } 314 | } 315 | 316 | renderEncoder.setFragmentTexture(colorMap, index: TextureIndex.color.rawValue) 317 | 318 | for submesh in mesh.submeshes { 319 | renderEncoder.drawIndexedPrimitives(type: submesh.primitiveType, 320 | indexCount: submesh.indexCount, 321 | indexType: submesh.indexType, 322 | indexBuffer: submesh.indexBuffer.buffer, 323 | indexBufferOffset: submesh.indexBuffer.offset) 324 | 325 | } 326 | 327 | renderEncoder.popDebugGroup() 328 | 329 | renderEncoder.endEncoding() 330 | 331 | if let drawable = view.currentDrawable { 332 | commandBuffer.present(drawable) 333 | } 334 | } 335 | 336 | commandBuffer.commit() 337 | } 338 | } 339 | 340 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 341 | /// Respond to drawable size or orientation changes here 342 | 343 | let aspect = Float(size.width) / Float(size.height) 344 | projectionMatrix = matrix_perspective_right_hand(fovyRadians: radians_from_degrees(65), aspectRatio:aspect, nearZ: 0.1, farZ: 100.0) 345 | } 346 | } 347 | 348 | // Generic matrix math utility functions 349 | func matrix4x4_rotation(radians: Float, axis: SIMD3) -> matrix_float4x4 { 350 | let unitAxis = normalize(axis) 351 | let ct = cosf(radians) 352 | let st = sinf(radians) 353 | let ci = 1 - ct 354 | let x = unitAxis.x, y = unitAxis.y, z = unitAxis.z 355 | return matrix_float4x4.init(columns:(vector_float4( ct + x * x * ci, y * x * ci + z * st, z * x * ci - y * st, 0), 356 | vector_float4(x * y * ci - z * st, ct + y * y * ci, z * y * ci + x * st, 0), 357 | vector_float4(x * z * ci + y * st, y * z * ci - x * st, ct + z * z * ci, 0), 358 | vector_float4( 0, 0, 0, 1))) 359 | } 360 | 361 | func matrix4x4_translation(_ translationX: Float, _ translationY: Float, _ translationZ: Float) -> matrix_float4x4 { 362 | return matrix_float4x4.init(columns:(vector_float4(1, 0, 0, 0), 363 | vector_float4(0, 1, 0, 0), 364 | vector_float4(0, 0, 1, 0), 365 | vector_float4(translationX, translationY, translationZ, 1))) 366 | } 367 | 368 | func matrix_perspective_right_hand(fovyRadians fovy: Float, aspectRatio: Float, nearZ: Float, farZ: Float) -> matrix_float4x4 { 369 | let ys = 1 / tanf(fovy * 0.5) 370 | let xs = ys / aspectRatio 371 | let zs = farZ / (nearZ - farZ) 372 | return matrix_float4x4.init(columns:(vector_float4(xs, 0, 0, 0), 373 | vector_float4( 0, ys, 0, 0), 374 | vector_float4( 0, 0, zs, -1), 375 | vector_float4( 0, 0, zs * nearZ, 0))) 376 | } 377 | 378 | func radians_from_degrees(_ degrees: Float) -> Float { 379 | return (degrees / 180) * .pi 380 | } 381 | */ 382 | -------------------------------------------------------------------------------- /metalRay Shared/Draw/Draw.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Metal.metal 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | #import "../Bridge.h" 12 | 13 | struct VertexIn { 14 | float2 position [[attribute(0)]]; 15 | float2 textureCoordinate [[attribute(1)]]; 16 | float4 color [[attribute(2)]]; 17 | }; 18 | 19 | struct VertexOut { 20 | float4 position [[position]]; 21 | float2 textureCoordinate; 22 | float4 color; 23 | }; 24 | 25 | typedef struct 26 | { 27 | float4 clipSpacePosition [[position]]; 28 | float2 textureCoordinate; 29 | } RasterizerData; 30 | 31 | // Quad Vertex Function 32 | vertex VertexOut poly2DVertex(uint vertexID [[ vertex_id ]], 33 | VertexIn in [[stage_in]], 34 | constant vector_uint2 *viewportSizePointer [[ buffer(1) ]]) 35 | { 36 | VertexOut out; 37 | 38 | float2 viewportSize = float2(*viewportSizePointer); 39 | 40 | out.position.xy = in.position / (viewportSize / 2.0); 41 | out.position.z = 0.0; 42 | out.position.w = 1.0; 43 | 44 | out.textureCoordinate = in.textureCoordinate; 45 | out.color = in.color; 46 | return out; 47 | } 48 | 49 | fragment float4 poly2DFragment(VertexOut in [[stage_in]], 50 | constant RectUniform *data [[ buffer(0) ]], 51 | texture2d inTexture [[ texture(1) ]] ) 52 | { 53 | float4 color = in.color; 54 | 55 | if (data->hasTexture == 1) { 56 | constexpr sampler textureSampler (mag_filter::linear, 57 | min_filter::linear); 58 | float2 uv = in.textureCoordinate; 59 | uv.y = 1 - uv.y; 60 | color = float4(inTexture.sample(textureSampler, uv)); 61 | } 62 | /* 63 | float2 uv = in.textureCoordinate * ( data->size ); 64 | uv -= float2( data->size / 2.0 ); 65 | 66 | float2 d = abs( uv ) - data->size / 2 + data->onion + data->round; 67 | float dist = length(max(d,float2(0))) + min(max(d.x,d.y),0.0) - data->round; 68 | 69 | if (data->onion > 0.0) 70 | dist = abs(dist) - data->onion; 71 | 72 | const float mask = m4mFillMask( dist ); 73 | float4 col = float4( data->fillColor.xyz, data->fillColor.w * mask); 74 | 75 | if (data->hasTexture == 1 && col.w > 0.0) { 76 | constexpr sampler textureSampler (mag_filter::linear, 77 | min_filter::linear); 78 | 79 | float2 uv = in.textureCoordinate; 80 | uv.y = 1 - uv.y; 81 | uv = m4mRotateCCWPivot(uv, data->rotation, 0.5); 82 | 83 | float4 sample = float4(inTexture.sample(textureSampler, uv)); 84 | 85 | col.xyz = sample.xyz; 86 | col.w = col.w * sample.w; 87 | } 88 | 89 | float borderMask = m4mBorderMask(dist, data->borderSize); 90 | float4 borderColor = data->borderColor; 91 | borderColor.w *= borderMask; 92 | col = mix( col, borderColor, borderMask );*/ 93 | 94 | return color; 95 | } 96 | 97 | // --- SDF utilities 98 | 99 | float m4mFillMask(float dist) 100 | { 101 | return clamp(-dist, 0.0, 1.0); 102 | } 103 | 104 | float m4mBorderMask(float dist, float width) 105 | { 106 | dist += 1.0; 107 | return clamp(dist + width, 0.0, 1.0) - clamp(dist, 0.0, 1.0); 108 | } 109 | 110 | float2 m4mRotateCCW(float2 pos, float angle) 111 | { 112 | float ca = cos(angle), sa = sin(angle); 113 | return pos * float2x2(ca, sa, -sa, ca); 114 | } 115 | 116 | float2 m4mRotateCCWPivot(float2 pos, float angle, float2 pivot) 117 | { 118 | float ca = cos(angle), sa = sin(angle); 119 | return pivot + (pos-pivot) * float2x2(ca, sa, -sa, ca); 120 | } 121 | 122 | float2 m4mRotateCW(float2 pos, float angle) 123 | { 124 | float ca = cos(angle), sa = sin(angle); 125 | return pos * float2x2(ca, -sa, sa, ca); 126 | } 127 | 128 | float2 m4mRotateCWPivot(float2 pos, float angle, float2 pivot) 129 | { 130 | float ca = cos(angle), sa = sin(angle); 131 | return pivot + (pos-pivot) * float2x2(ca, -sa, sa, ca); 132 | } 133 | 134 | float dot2( float2 v ) { return dot(v,v); } 135 | float cross2( float2 a, float2 b ) { return a.x*b.y - a.y*b.x; } 136 | 137 | // signed distance to a quadratic bezier, https://www.shadertoy.com/view/MlKcDD 138 | float sdBezier(float2 pos, float2 p0, float2 p1, float2 p2 ) 139 | { 140 | float2 a = p1 - p0; 141 | float2 b = p0 - 2.0*p1 + p2; 142 | float2 c = p0 - pos; 143 | 144 | float kk = 1.0 / dot(b,b); 145 | float kx = kk * dot(a,b); 146 | float ky = kk * (2.0*dot(a,a)+dot(c,b)) / 3.0; 147 | float kz = kk * dot(c,a); 148 | 149 | float2 res; 150 | 151 | float p = ky - kx*kx; 152 | float p3 = p*p*p; 153 | float q = kx*(2.0*kx*kx - 3.0*ky) + kz; 154 | float h = q*q + 4.0*p3; 155 | 156 | if(h >= 0.0) 157 | { 158 | h = sqrt(h); 159 | float2 x = (float2(h, -h) - q) / 2.0; 160 | float2 uv = sign(x)*pow(abs(x), float2(1.0/3.0)); 161 | float t = uv.x + uv.y - kx; 162 | t = clamp( t, 0.0, 1.0 ); 163 | 164 | // 1 root 165 | float2 qos = c + (2.0*a + b*t)*t; 166 | res = float2( length(qos),t); 167 | } else { 168 | float z = sqrt(-p); 169 | float v = acos( q/(p*z*2.0) ) / 3.0; 170 | float m = cos(v); 171 | float n = sin(v)*1.732050808; 172 | float3 t = float3(m + m, -n - m, n - m) * z - kx; 173 | t = clamp( t, 0.0, 1.0 ); 174 | 175 | // 3 roots 176 | float2 qos = c + (2.0*a + b*t.x)*t.x; 177 | float dis = dot(qos,qos); 178 | 179 | res = float2(dis,t.x); 180 | 181 | qos = c + (2.0*a + b*t.y)*t.y; 182 | dis = dot(qos,qos); 183 | if( dis inTexture [[ texture(1) ]] ) 198 | { 199 | float2 uv = in.textureCoordinate * float2( data->radius * 2 + data->borderSize, data->radius * 2 + data->borderSize); 200 | uv -= float2( data->radius + data->borderSize / 2 ); 201 | 202 | float dist = length( uv ) - data->radius + data->onion; 203 | if (data->onion > 0.0) 204 | dist = abs(dist) - data->onion; 205 | 206 | const float mask = m4mFillMask( dist ); 207 | float4 col = float4( data->fillColor.xyz, data->fillColor.w * mask); 208 | 209 | float borderMask = m4mBorderMask(dist, data->borderSize); 210 | float4 borderColor = data->borderColor; 211 | borderColor.w *= borderMask; 212 | col = mix( col, borderColor, borderMask ); 213 | 214 | if (data->hasTexture == 1 && col.w > 0.0) { 215 | constexpr sampler textureSampler (mag_filter::linear, 216 | min_filter::linear); 217 | 218 | float2 uv = in.textureCoordinate; 219 | uv.y = 1 - uv.y; 220 | uv = m4mRotateCCWPivot(uv, data->rotation, 0.5); 221 | 222 | float4 sample = float4(inTexture.sample(textureSampler, uv)); 223 | 224 | col.xyz = sample.xyz; 225 | col.w = col.w * sample.w; 226 | } 227 | 228 | return col; 229 | } 230 | 231 | // Box 232 | fragment float4 m4mBoxDrawable(RasterizerData in [[stage_in]], 233 | constant BoxUniform *data [[ buffer(0) ]], 234 | texture2d inTexture [[ texture(1) ]] ) 235 | { 236 | float2 uv = in.textureCoordinate * ( data->size ); 237 | uv -= float2( data->size / 2.0 ); 238 | 239 | float2 d = abs( uv ) - data->size / 2 + data->onion + data->round; 240 | float dist = length(max(d,float2(0))) + min(max(d.x,d.y),0.0) - data->round; 241 | 242 | if (data->onion > 0.0) 243 | dist = abs(dist) - data->onion; 244 | 245 | const float mask = m4mFillMask( dist ); 246 | float4 col = float4( data->fillColor.xyz, data->fillColor.w * mask); 247 | 248 | if (data->hasTexture == 1 && col.w > 0.0) { 249 | constexpr sampler textureSampler (mag_filter::linear, 250 | min_filter::linear); 251 | 252 | float2 uv = in.textureCoordinate; 253 | uv.y = 1 - uv.y; 254 | uv = m4mRotateCCWPivot(uv, data->rotation, 0.5); 255 | 256 | float4 sample = float4(inTexture.sample(textureSampler, uv)); 257 | 258 | col.xyz = sample.xyz; 259 | col.w = col.w * sample.w; 260 | } 261 | 262 | float borderMask = m4mBorderMask(dist, data->borderSize); 263 | float4 borderColor = data->borderColor; 264 | borderColor.w *= borderMask; 265 | col = mix( col, borderColor, borderMask ); 266 | 267 | return col; 268 | } 269 | 270 | // Line 271 | fragment float4 m4mLineDrawable(RasterizerData in [[stage_in]], 272 | constant LineUniform *data [[ buffer(0) ]]) 273 | { 274 | float2 uv = in.textureCoordinate * ( data->size + data->borderSize / 2.0); 275 | uv -= float2(data->size / 2.0 + data->borderSize / 2.0); 276 | 277 | float2 o = uv - data->sp; 278 | float2 l = data->ep - data->sp; 279 | 280 | float h = clamp( dot(o,l)/dot(l,l), 0.0, 1.0 ); 281 | float dist = -(data->width-distance(o,l*h)); 282 | 283 | float4 col = float4( data->fillColor.x, data->fillColor.y, data->fillColor.z, m4mFillMask( dist ) * data->fillColor.w ); 284 | col = mix( col, data->borderColor, m4mBorderMask( dist, data->borderSize ) ); 285 | 286 | return col; 287 | } 288 | 289 | // Bezier 290 | fragment float4 m4mBezierDrawable(RasterizerData in [[stage_in]], 291 | constant BezierUniform *data [[ buffer(0) ]]) 292 | { 293 | float2 uv = in.textureCoordinate * ( data->size + data->borderSize * 2.0); 294 | uv -= float2(data->size / 2.0 + data->borderSize / 2.0); 295 | 296 | float2 p1 = data->p1; 297 | float2 p2 = data->p2; 298 | float2 p3 = data->p3; 299 | 300 | float dist = sdBezier(uv, p1, p2, p3) - data->width; 301 | 302 | float4 col = float4( data->fillColor.x, data->fillColor.y, data->fillColor.z, m4mFillMask( dist ) * data->fillColor.w ); 303 | col = mix( col, data->borderColor, m4mBorderMask( dist, data->borderSize ) ); 304 | 305 | return col; 306 | } 307 | 308 | // Rotated Box 309 | fragment float4 m4mBoxDrawableExt(RasterizerData in [[stage_in]], 310 | constant BoxUniform *data [[ buffer(0) ]], 311 | texture2d inTexture [[ texture(1) ]] ) 312 | { 313 | float2 uv = in.textureCoordinate * data->screenSize; 314 | uv.y = data->screenSize.y - uv.y; 315 | uv -= float2(data->size / 2.0); 316 | uv -= float2(data->pos.x, data->pos.y); 317 | 318 | uv = m4mRotateCCW(uv, data->rotation); 319 | 320 | float2 d = abs( uv ) - data->size / 2.0 + data->onion + data->round;// - data->borderSize; 321 | float dist = length(max(d,float2(0))) + min(max(d.x,d.y),0.0) - data->round; 322 | 323 | if (data->onion > 0.0) 324 | dist = abs(dist) - data->onion; 325 | 326 | const float mask = m4mFillMask( dist );//smoothstep(0.0, pixelSize, -dist); 327 | float4 col = float4( data->fillColor.xyz, data->fillColor.w * mask); 328 | 329 | const float borderMask = m4mBorderMask(dist, data->borderSize); 330 | float4 borderColor = data->borderColor; 331 | borderColor.w *= borderMask; 332 | col = mix( col, borderColor, borderMask ); 333 | 334 | if (data->hasTexture == 1 && col.w > 0.0) { 335 | constexpr sampler textureSampler (mag_filter::linear, 336 | min_filter::linear); 337 | 338 | float2 uv = in.textureCoordinate; 339 | uv.y = 1 - uv.y; 340 | 341 | uv -= data->pos / data->screenSize; 342 | uv *= data->screenSize / data->size; 343 | 344 | uv = m4mRotateCCWPivot(uv, data->rotation, (data->size / 2.0) / data->screenSize * (data->screenSize / data->size)); 345 | 346 | float4 sample = float4(inTexture.sample(textureSampler, uv)); 347 | 348 | col.xyz = sample.xyz; 349 | col.w = col.w * sample.w; 350 | } 351 | 352 | return col; 353 | } 354 | 355 | // --- Box Drawable 356 | fragment float4 m4mBoxPatternDrawable(RasterizerData in [[stage_in]], 357 | constant BoxUniform *data [[ buffer(0) ]] ) 358 | { 359 | float2 uv = in.textureCoordinate * ( data->screenSize ); 360 | uv -= float2( data->screenSize / 2.0 ); 361 | 362 | float2 d = abs( uv ) - data->size / 2.0; 363 | float dist = length(max(d,float2(0))) + min(max(d.x,d.y),0.0); 364 | 365 | float4 checkerColor1 = data->fillColor; 366 | float4 checkerColor2 = data->borderColor; 367 | 368 | //uv = fragCoord; 369 | //uv -= float2( data->size / 2 ); 370 | 371 | float4 col = checkerColor1; 372 | 373 | float cWidth = 12.0; 374 | float cHeight = 12.0; 375 | 376 | if ( fmod( floor( uv.x / cWidth ), 2.0 ) == 0.0 ) { 377 | if ( fmod( floor( uv.y / cHeight ), 2.0 ) != 0.0 ) col=checkerColor2; 378 | } else { 379 | if ( fmod( floor( uv.y / cHeight ), 2.0 ) == 0.0 ) col=checkerColor2; 380 | } 381 | 382 | return float4( col.xyz, m4mFillMask( dist ) ); 383 | } 384 | 385 | // --- Grid Drawable 386 | fragment float4 m4mGridDrawable(RasterizerData in [[stage_in]], 387 | constant GridUniform *data [[ buffer(0) ]] ) 388 | { 389 | float2 uv = in.textureCoordinate * ( data->screenSize ); 390 | // uv -= float2( data->screenSize / 2.0 ); 391 | 392 | float4 col = data->backColor; 393 | 394 | float tile_half_x = data->gridSize / 2.0; 395 | float tile_half_y = data->gridSize / 4.0; 396 | 397 | float offset_from_0_x = uv.x - (data->screenSize.x / 2.0 + data->offset.x); 398 | float offset_from_0_y = uv.y - (data->screenSize.y / 2.0 + data->offset.y); 399 | 400 | float grid_x = offset_from_0_x / tile_half_x; 401 | float grid_y = offset_from_0_y / tile_half_y; 402 | 403 | float grid_x_screen = grid_x * tile_half_x; 404 | float grid_y_screen = grid_y * tile_half_y; 405 | 406 | float map_x = grid_x_screen / data->gridSize * 2.0; 407 | float map_y = grid_y_screen / data->gridSize * 2.0; 408 | 409 | float c_x = cos(map_x * M_PI_F * 2.0); 410 | float c_y = cos(map_y * M_PI_F * 2.0); 411 | float v = smoothstep(0.99, 1.0, max(c_x,c_y)); 412 | 413 | // float2 coord = cos(M_PI_F/data->gridSize * uv); 414 | // float v = smoothstep(0.999, 1.0, max(coord.x, coord.y)); 415 | // col = mix(col, data->gridColor, v); 416 | 417 | col = mix(col, data->gridColor, v); 418 | 419 | return float4( col ); 420 | } 421 | 422 | // Copy texture 423 | fragment float4 m4mCopyTextureDrawable(RasterizerData in [[stage_in]], 424 | constant TextureUniform *data [[ buffer(0) ]], 425 | texture2d inTexture [[ texture(1) ]]) 426 | { 427 | float2 uv = in.textureCoordinate * data->size; 428 | uv.y = data->size.y - uv.y; 429 | 430 | const half4 colorSample = inTexture.read(uint2(uv)); 431 | float4 sample = float4( colorSample ); 432 | 433 | sample.w *= data->globalAlpha; 434 | 435 | return float4(sample.x / sample.w, sample.y / sample.w, sample.z / sample.w, sample.w); 436 | } 437 | 438 | fragment float4 m4mTextureDrawable(RasterizerData in [[stage_in]], 439 | constant TextureUniform *data [[ buffer(0) ]], 440 | texture2d inTexture [[ texture(1) ]]) 441 | { 442 | //constexpr sampler textureSampler (mag_filter::linear, 443 | // min_filter::linear); 444 | 445 | constexpr sampler textureSampler (mag_filter::linear, 446 | min_filter::linear); 447 | float2 uv = in.textureCoordinate; 448 | uv.y = 1 - uv.y; 449 | 450 | uv.x *= data->size.x; 451 | uv.y *= data->size.y; 452 | 453 | uv.x += data->pos.x; 454 | uv.y += data->pos.y; 455 | 456 | float4 sample = float4(inTexture.sample(textureSampler, uv)); 457 | sample.w *= data->globalAlpha; 458 | 459 | return sample; 460 | } 461 | 462 | float m4mMedian(float r, float g, float b) { 463 | return max(min(r, g), min(max(r, g), b)); 464 | } 465 | 466 | fragment float4 m4mTextDrawable(VertexOut in [[stage_in]], 467 | constant TextUniform *data [[ buffer(0) ]], 468 | texture2d inTexture [[ texture(1) ]]) 469 | { 470 | float4 color = in.color; 471 | 472 | constexpr sampler textureSampler (mag_filter::linear, 473 | min_filter::linear); 474 | 475 | float2 uv = in.textureCoordinate; 476 | uv.y = 1 - uv.y; 477 | 478 | uv /= data->atlasSize / data->fontSize; 479 | uv += data->fontPos / data->atlasSize; 480 | 481 | float4 sample = inTexture.sample(textureSampler, uv ); 482 | 483 | float d = m4mMedian(sample.r, sample.g, sample.b) - 0.5; 484 | float w = clamp(d/fwidth(d) + 0.5, 0.0, 1.0); 485 | 486 | color.w *= w; 487 | 488 | return color; 489 | } 490 | 491 | kernel void makeCGIImage( 492 | texture2d outTexture [[texture(0)]], 493 | texture2d inTexture [[texture(2)]], 494 | uint2 gid [[thread_position_in_grid]]) 495 | { 496 | //float2 size = float2( outTexture.get_width(), outTexture.get_height() ); 497 | half4 color = inTexture.read(gid).zyxw; 498 | color.xyz = pow(color.xyz, 2.2); 499 | outTexture.write(color, gid); 500 | } 501 | -------------------------------------------------------------------------------- /metalRay Shared/Draw/MetalDraw2D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalDrawables.swift 3 | // metalRay 4 | // 5 | // Created by Markus Moenig on 21/8/23. 6 | // 7 | 8 | import MetalKit 9 | 10 | extension MTLVertexDescriptor { 11 | static var defaultLayout: MTLVertexDescriptor { 12 | let vertexDescriptor = MTLVertexDescriptor() 13 | 14 | vertexDescriptor.attributes[0].format = .float2 15 | vertexDescriptor.attributes[0].offset = 0 16 | vertexDescriptor.attributes[0].bufferIndex = 0 17 | 18 | vertexDescriptor.attributes[1].format = .float2 19 | vertexDescriptor.attributes[1].offset = MemoryLayout.stride * 2 20 | vertexDescriptor.attributes[1].bufferIndex = 0 21 | 22 | vertexDescriptor.attributes[2].format = .float4 23 | vertexDescriptor.attributes[2].offset = MemoryLayout.stride * 4 24 | vertexDescriptor.attributes[2].bufferIndex = 0 25 | 26 | let stride = MemoryLayout.stride * 8 27 | vertexDescriptor.layouts[0].stride = stride 28 | return vertexDescriptor 29 | } 30 | } 31 | 32 | class MetalDraw2D 33 | { 34 | let metalView : RayView 35 | 36 | let device : MTLDevice 37 | let commandQueue : MTLCommandQueue! 38 | 39 | var pipelineState : MTLRenderPipelineState! = nil 40 | var pipelineStateDesc : MTLRenderPipelineDescriptor! = nil 41 | 42 | var renderEncoder : MTLRenderCommandEncoder! = nil 43 | 44 | var vertexBuffer : MTLBuffer? = nil 45 | var viewportSize : vector_uint2 46 | 47 | var commandBuffer : MTLCommandBuffer! = nil 48 | 49 | var polyState : MTLRenderPipelineState? = nil 50 | var textState : MTLRenderPipelineState? = nil 51 | 52 | var scaleFactor : Float 53 | var viewSize = float2(0,0) 54 | 55 | var vertexData : [Float] = [] 56 | var vertexCount : Int = 0 57 | 58 | var primitiveType : MTLPrimitiveType = .triangle 59 | 60 | var textures : [Int:MTLTexture] = [:] 61 | var textureIdCount : Int = 1 62 | 63 | var target : Int? = nil 64 | var texture : Int? = nil 65 | 66 | var fonts : [String:Font] = [:] 67 | var font : Font! = nil 68 | 69 | init(_ metalView: RayView) 70 | { 71 | self.metalView = metalView 72 | #if os(iOS) 73 | metalView.layer.isOpaque = false 74 | #elseif os(macOS) 75 | metalView.layer?.isOpaque = false 76 | #endif 77 | 78 | device = metalView.device! 79 | viewportSize = vector_uint2( UInt32(metalView.bounds.width), UInt32(metalView.bounds.height) ) 80 | commandQueue = device.makeCommandQueue() 81 | 82 | scaleFactor = metalView.game.scaleFactor 83 | 84 | if let defaultLibrary = device.makeDefaultLibrary() { 85 | 86 | pipelineStateDesc = MTLRenderPipelineDescriptor() 87 | let vertexFunction = defaultLibrary.makeFunction( name: "poly2DVertex" ) 88 | pipelineStateDesc.vertexFunction = vertexFunction 89 | pipelineStateDesc.colorAttachments[0].pixelFormat = metalView.colorPixelFormat 90 | 91 | pipelineStateDesc.vertexDescriptor = MTLVertexDescriptor.defaultLayout 92 | 93 | pipelineStateDesc.colorAttachments[0].isBlendingEnabled = true 94 | pipelineStateDesc.colorAttachments[0].rgbBlendOperation = .add 95 | pipelineStateDesc.colorAttachments[0].alphaBlendOperation = .add 96 | pipelineStateDesc.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha 97 | pipelineStateDesc.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha 98 | pipelineStateDesc.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha 99 | pipelineStateDesc.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha 100 | 101 | func createNewPipelineState(_ fragmentFunction: MTLFunction?) -> MTLRenderPipelineState? 102 | { 103 | if let function = fragmentFunction { 104 | pipelineStateDesc.fragmentFunction = function 105 | do { 106 | let renderPipelineState = try device.makeRenderPipelineState(descriptor: pipelineStateDesc) 107 | return renderPipelineState 108 | } catch { 109 | print( "createNewPipelineState failed" ) 110 | return nil 111 | } 112 | } 113 | return nil 114 | } 115 | 116 | var function = defaultLibrary.makeFunction( name: "poly2DFragment" ) 117 | polyState = createNewPipelineState(function) 118 | 119 | function = defaultLibrary.makeFunction( name: "m4mTextDrawable" ) 120 | textState = createNewPipelineState(function) 121 | } 122 | 123 | // Init the SDF fonts 124 | 125 | var font = Font(name: "OpenSans", game: metalView.game) 126 | fonts["opensans"] = font 127 | 128 | font = Font(name: "Square", game: metalView.game) 129 | fonts["square"] = font 130 | 131 | self.font = font 132 | } 133 | 134 | @discardableResult func encodeStart(_ clearColor: float4 = float4(0.125, 0.129, 0.137, 1)) -> MTLRenderCommandEncoder? 135 | { 136 | viewportSize = vector_uint2( UInt32(metalView.bounds.width), UInt32(metalView.bounds.height) ) 137 | viewSize = float2(Float(metalView.bounds.width), Float(metalView.bounds.height)) 138 | 139 | commandBuffer = commandQueue.makeCommandBuffer()! 140 | var renderPassDescriptor : MTLRenderPassDescriptor? 141 | 142 | if target == nil { 143 | renderPassDescriptor = metalView.currentRenderPassDescriptor 144 | } else { 145 | renderPassDescriptor = MTLRenderPassDescriptor() 146 | renderPassDescriptor?.colorAttachments[0].texture = textures[target!] 147 | } 148 | 149 | // renderPassDescriptor!.colorAttachments[0].loadAction = .clear 150 | // renderPassDescriptor!.colorAttachments[0].clearColor = MTLClearColor( red: Double(clearColor.x), green: Double(clearColor.y), blue: Double(clearColor.z), alpha: Double(clearColor.w)) 151 | // 152 | renderPassDescriptor!.colorAttachments[0].loadAction = .load 153 | 154 | if renderPassDescriptor != nil { 155 | renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor! ) 156 | return renderEncoder 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func encodeRun( _ renderEncoder: MTLRenderCommandEncoder, pipelineState: MTLRenderPipelineState? ) 163 | { 164 | renderEncoder.setRenderPipelineState( pipelineState! ) 165 | renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) 166 | } 167 | 168 | func encodeEnd() 169 | { 170 | renderEncoder?.endEncoding() 171 | 172 | if target == nil { 173 | guard let drawable = metalView.currentDrawable else { 174 | return 175 | } 176 | 177 | if let commandBuffer = commandBuffer { 178 | //commandBuffer.addCompletedHandler { cb in 179 | // print("Rendering Time:", (cb.gpuEndTime - cb.gpuStartTime) * 1000) 180 | //} 181 | commandBuffer.present(drawable) 182 | commandBuffer.commit() 183 | } 184 | } else { 185 | commandBuffer.commit() 186 | } 187 | } 188 | 189 | func startShape(type: MTLPrimitiveType) { 190 | 191 | primitiveType = type 192 | 193 | vertexData = [] 194 | vertexCount = 0 195 | } 196 | 197 | func addVertex(_ vertex: float2,_ textureCoordinate: float2,_ color: float4) { 198 | vertexData.append(-viewSize.x / 2.0 + vertex.x * scaleFactor) 199 | vertexData.append(viewSize.y / 2.0 - vertex.y * scaleFactor) 200 | vertexData.append(textureCoordinate.x) 201 | vertexData.append(textureCoordinate.y) 202 | vertexData.append(color.x) 203 | vertexData.append(color.y) 204 | vertexData.append(color.z) 205 | vertexData.append(color.w) 206 | vertexCount += 1 207 | } 208 | 209 | func drawRect(_ rect: Rectangle,_ c: float4, _ rot: Float) { 210 | 211 | // right, bottom, 1.0, 0.0, 212 | // left, bottom, 0.0, 0.0, 213 | // left, top, 0.0, 1.0, 214 | // 215 | // right, bottom, 1.0, 0.0, 216 | // left, top, 0.0, 1.0, 217 | // right, top, 1.0, 1.0, 218 | 219 | if rot == 0.0 { 220 | let arr : [Float ] = [ 221 | xToMetal(rect.x + rect.width), yToMetal(rect.y + rect.height), 1.0, 0.0, c.x, c.y, c.z, c.w, 222 | xToMetal(rect.x), yToMetal(rect.y + rect.height), 0.0, 0.0, c.x, c.y, c.z, c.w, 223 | xToMetal(rect.x), yToMetal(rect.y), 0.0, 1.0, c.x, c.y, c.z, c.w, 224 | 225 | xToMetal(rect.x + rect.width), yToMetal(rect.y + rect.height), 1.0, 0.0, c.x, c.y, c.z, c.w, 226 | xToMetal(rect.x), yToMetal(rect.y), 0.0, 1.0, c.x, c.y, c.z, c.w, 227 | xToMetal(rect.x + rect.width), yToMetal(rect.y), 1.0, 1.0, c.x, c.y, c.z, c.w, 228 | ] 229 | 230 | vertexData.append(contentsOf: arr) 231 | vertexCount += 6 232 | } else { 233 | 234 | let radians = rot.degreesToRadians 235 | let cos = cos(radians) 236 | let sin = sin(radians) 237 | let cx = rect.x + rect.width / 2.0 238 | let cy = rect.y + rect.height / 2.0 239 | 240 | func rotate(x : Float, y : Float) -> (Float, Float) { 241 | let nx = (cos * (x - cx)) + (sin * (y - cy)) + cx 242 | let ny = (cos * (y - cy)) - (sin * (x - cx)) + cy 243 | return (nx, ny) 244 | } 245 | 246 | let topLeft = rotate(x: rect.x, y: rect.y) 247 | let topRight = rotate(x: rect.x + rect.width, y: rect.y) 248 | let bottomLeft = rotate(x: rect.x, y: rect.y + rect.height) 249 | let bottomRight = rotate(x: rect.x + rect.width, y: rect.y + rect.height) 250 | 251 | let arr : [Float ] = [ 252 | xToMetal(bottomRight.0), yToMetal(bottomRight.1), 1.0, 0.0, c.x, c.y, c.z, c.w, 253 | xToMetal(bottomLeft.0), yToMetal(bottomLeft.1), 0.0, 0.0, c.x, c.y, c.z, c.w, 254 | xToMetal(topLeft.0), yToMetal(topLeft.1), 0.0, 1.0, c.x, c.y, c.z, c.w, 255 | 256 | xToMetal(bottomRight.0), yToMetal(bottomRight.1), 1.0, 0.0, c.x, c.y, c.z, c.w, 257 | xToMetal(topLeft.0), yToMetal(topLeft.1), 0.0, 1.0, c.x, c.y, c.z, c.w, 258 | xToMetal(topRight.0), yToMetal(topRight.1), 1.0, 1.0, c.x, c.y, c.z, c.w, 259 | ] 260 | 261 | vertexData.append(contentsOf: arr) 262 | vertexCount += 6 263 | } 264 | } 265 | 266 | func endShape() { 267 | 268 | if !vertexData.isEmpty { 269 | var data = RectUniform() 270 | data.hasTexture = 0; 271 | 272 | renderEncoder.setVertexBytes(vertexData, length: vertexData.count * MemoryLayout.stride, index: 0) 273 | renderEncoder.setVertexBytes(&viewportSize, length: MemoryLayout.stride, index: 1) 274 | 275 | if texture != nil { 276 | if let tex = textures[texture!] { 277 | data.hasTexture = 1 278 | renderEncoder.setFragmentTexture(tex, index: 1) 279 | } 280 | } 281 | renderEncoder.setFragmentBytes(&data, length: MemoryLayout.stride, index: 0) 282 | 283 | renderEncoder.setRenderPipelineState(polyState!) 284 | renderEncoder.drawPrimitives(type: primitiveType, vertexStart: 0, vertexCount: vertexCount) 285 | } 286 | 287 | vertexData = [] 288 | vertexCount = 0 289 | } 290 | 291 | /// Draws the given text 292 | func drawText(position: float2, text: String, size: Float, color: float4 = float4(1,1,1,1)) 293 | { 294 | func drawChar(char: BMChar, x: Float, y: Float, adjScale: Float) 295 | { 296 | var data = TextUniform() 297 | 298 | data.atlasSize.x = Float(font!.atlas!.width) 299 | data.atlasSize.y = Float(font!.atlas!.height) 300 | data.fontPos.x = char.x 301 | data.fontPos.y = char.y 302 | data.fontSize.x = char.width 303 | data.fontSize.y = char.height 304 | 305 | let rect = MRRect(x, y, char.width * adjScale, char.height * adjScale, scale: 1) 306 | 307 | let c = color 308 | 309 | vertexData = [ 310 | xToMetal(rect.x + rect.width), yToMetal(rect.y + rect.height), 1.0, 0.0, c.x, c.y, c.z, c.w, 311 | xToMetal(rect.x), yToMetal(rect.y + rect.height), 0.0, 0.0, c.x, c.y, c.z, c.w, 312 | xToMetal(rect.x), yToMetal(rect.y), 0.0, 1.0, c.x, c.y, c.z, c.w, 313 | 314 | xToMetal(rect.x + rect.width), yToMetal(rect.y + rect.height), 1.0, 0.0, c.x, c.y, c.z, c.w, 315 | xToMetal(rect.x), yToMetal(rect.y), 0.0, 1.0, c.x, c.y, c.z, c.w, 316 | xToMetal(rect.x + rect.width), yToMetal(rect.y), 1.0, 1.0, c.x, c.y, c.z, c.w, 317 | ] 318 | vertexCount = 6 319 | 320 | renderEncoder.setVertexBytes(vertexData, length: vertexData.count * MemoryLayout.stride, index: 0) 321 | renderEncoder.setVertexBytes(&viewportSize, length: MemoryLayout.stride, index: 1) 322 | 323 | renderEncoder.setFragmentBytes(&data, length: MemoryLayout.stride, index: 0) 324 | renderEncoder.setFragmentTexture(font!.atlas, index: 1) 325 | 326 | renderEncoder.setRenderPipelineState(textState!) 327 | renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) 328 | } 329 | 330 | if let font = font { 331 | 332 | let scale : Float = (1.0 / font.bmFont!.common.lineHeight) * size 333 | let adjScale : Float = scale// / 2 334 | 335 | var posX = position.x// / game.scaleFactor 336 | let posY = position.y// / game.scaleFactor 337 | 338 | for c in text { 339 | let bmChar = font.getItemForChar( c ) 340 | if bmChar != nil { 341 | drawChar(char: bmChar!, x: posX + bmChar!.xoffset * adjScale, y: posY + bmChar!.yoffset * adjScale, adjScale: adjScale) 342 | posX += bmChar!.xadvance * adjScale; 343 | } 344 | } 345 | } 346 | 347 | vertexData = [] 348 | vertexCount = 0 349 | } 350 | 351 | /// Sets the current font 352 | func setFont(name: String) -> Bool 353 | { 354 | if let font = fonts[name] { 355 | self.font = font 356 | return true 357 | } 358 | return false 359 | } 360 | 361 | /// Gets the width of the given text 362 | func getTextSize(text: String, size: Float) -> float2 363 | { 364 | var rc = float2() 365 | 366 | if let font = font { 367 | 368 | let scale : Float = (1.0 / font.bmFont!.common.lineHeight) * size 369 | let adjScale : Float = scale// / 2 370 | 371 | var posX : Float = 0 372 | 373 | for c in text { 374 | let bmChar = font.getItemForChar( c ) 375 | if bmChar != nil { 376 | posX += bmChar!.xadvance * adjScale 377 | } 378 | } 379 | 380 | rc.x = posX 381 | rc.y = font.bmFont!.common.lineHeight 382 | } 383 | return rc 384 | } 385 | 386 | /// Updates the view 387 | func update() { 388 | metalView.enableSetNeedsDisplay = true 389 | #if os(OSX) 390 | let nsrect : NSRect = NSRect(x:0, y: 0, width: metalView.frame.width, height: metalView.frame.height) 391 | metalView.setNeedsDisplay(nsrect) 392 | #else 393 | metalView.setNeedsDisplay() 394 | #endif 395 | } 396 | 397 | /// Create a texture and return its id 398 | func createTexture(width: Int, height: Int) -> Int? 399 | { 400 | let textureDescriptor = MTLTextureDescriptor() 401 | textureDescriptor.textureType = MTLTextureType.type2D 402 | textureDescriptor.pixelFormat = MTLPixelFormat.bgra8Unorm 403 | textureDescriptor.width = width == 0 ? 1 : width 404 | textureDescriptor.height = height == 0 ? 1 : height 405 | 406 | textureDescriptor.usage = MTLTextureUsage.unknown 407 | 408 | guard let texture = device.makeTexture(descriptor: textureDescriptor) else { 409 | return nil 410 | } 411 | 412 | let id = textureIdCount 413 | textures[id] = texture 414 | textureIdCount += 1 415 | return id 416 | } 417 | 418 | /// Sets the render target 419 | @discardableResult func setTarget(id: Int) -> Bool { 420 | if id <= 0 { 421 | target = nil 422 | } else { 423 | if textures.keys.contains(id) == false { 424 | return false 425 | } else { 426 | target = id 427 | } 428 | } 429 | return true 430 | } 431 | 432 | /// Sets the current texture 433 | @discardableResult func setTexture(id: Int) -> Bool { 434 | if id <= 0 { 435 | texture = nil 436 | } else { 437 | if textures.keys.contains(id) == false { 438 | return false 439 | } else { 440 | texture = id 441 | } 442 | } 443 | return true 444 | } 445 | 446 | /// LoadTexture 447 | func loadTexture(_ name: String, mipmaps: Bool = false, sRGB: Bool = false) -> Int? 448 | { 449 | let path = Bundle.main.path(forResource: name, ofType: "tiff")! 450 | let data = NSData(contentsOfFile: path)! as Data 451 | 452 | let options: [MTKTextureLoader.Option : Any] = [.generateMipmaps : mipmaps, .SRGB : sRGB] 453 | 454 | if let texture = try? metalView.game.textureLoader.newTexture(data: data, options: options) { 455 | let id = textureIdCount 456 | textures[id] = texture 457 | textureIdCount += 1 458 | return id 459 | } 460 | return nil 461 | } 462 | 463 | func xToMetal(_ v: Float) -> Float { 464 | -viewSize.x / 2.0 + v// * scaleFactor 465 | } 466 | 467 | func yToMetal(_ v: Float) -> Float { 468 | viewSize.y / 2.0 - v// * scaleFactor 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /metalRay Shared/Fonts/Square.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "Square.png" 4 | ], 5 | "chars": [ 6 | { 7 | "id": 124, 8 | "index": 95, 9 | "char": "|", 10 | "width": 10, 11 | "height": 78, 12 | "xoffset": 2, 13 | "yoffset": -7, 14 | "xadvance": 14, 15 | "chnl": 15, 16 | "x": 0, 17 | "y": 0, 18 | "page": 0 19 | }, 20 | { 21 | "id": 36, 22 | "index": 7, 23 | "char": "$", 24 | "width": 44, 25 | "height": 76, 26 | "xoffset": 2, 27 | "yoffset": -4, 28 | "xadvance": 49, 29 | "chnl": 15, 30 | "x": 11, 31 | "y": 0, 32 | "page": 0 33 | }, 34 | { 35 | "id": 113, 36 | "index": 84, 37 | "char": "q", 38 | "width": 44, 39 | "height": 73, 40 | "xoffset": 2, 41 | "yoffset": 2, 42 | "xadvance": 49, 43 | "chnl": 15, 44 | "x": 56, 45 | "y": 0, 46 | "page": 0 47 | }, 48 | { 49 | "id": 81, 50 | "index": 52, 51 | "char": "Q", 52 | "width": 44, 53 | "height": 73, 54 | "xoffset": 2, 55 | "yoffset": 2, 56 | "xadvance": 49, 57 | "chnl": 15, 58 | "x": 56, 59 | "y": 74, 60 | "page": 0 61 | }, 62 | { 63 | "id": 76, 64 | "index": 47, 65 | "char": "L", 66 | "width": 44, 67 | "height": 63, 68 | "xoffset": 2, 69 | "yoffset": 2, 70 | "xadvance": 49, 71 | "chnl": 15, 72 | "x": 11, 73 | "y": 77, 74 | "page": 0 75 | }, 76 | { 77 | "id": 38, 78 | "index": 9, 79 | "char": "&", 80 | "width": 44, 81 | "height": 63, 82 | "xoffset": 2, 83 | "yoffset": 2, 84 | "xadvance": 49, 85 | "chnl": 15, 86 | "x": 101, 87 | "y": 0, 88 | "page": 0 89 | }, 90 | { 91 | "id": 35, 92 | "index": 6, 93 | "char": "#", 94 | "width": 44, 95 | "height": 63, 96 | "xoffset": 2, 97 | "yoffset": 2, 98 | "xadvance": 49, 99 | "chnl": 15, 100 | "x": 101, 101 | "y": 64, 102 | "page": 0 103 | }, 104 | { 105 | "id": 40, 106 | "index": 11, 107 | "char": "(", 108 | "width": 21, 109 | "height": 63, 110 | "xoffset": 2, 111 | "yoffset": 2, 112 | "xadvance": 25, 113 | "chnl": 15, 114 | "x": 146, 115 | "y": 0, 116 | "page": 0 117 | }, 118 | { 119 | "id": 41, 120 | "index": 12, 121 | "char": ")", 122 | "width": 21, 123 | "height": 63, 124 | "xoffset": 2, 125 | "yoffset": 2, 126 | "xadvance": 25, 127 | "chnl": 15, 128 | "x": 146, 129 | "y": 64, 130 | "page": 0 131 | }, 132 | { 133 | "id": 123, 134 | "index": 94, 135 | "char": "{", 136 | "width": 26, 137 | "height": 63, 138 | "xoffset": 2, 139 | "yoffset": 2, 140 | "xadvance": 30, 141 | "chnl": 15, 142 | "x": 0, 143 | "y": 148, 144 | "page": 0 145 | }, 146 | { 147 | "id": 117, 148 | "index": 88, 149 | "char": "u", 150 | "width": 44, 151 | "height": 63, 152 | "xoffset": 2, 153 | "yoffset": 2, 154 | "xadvance": 49, 155 | "chnl": 15, 156 | "x": 27, 157 | "y": 148, 158 | "page": 0 159 | }, 160 | { 161 | "id": 122, 162 | "index": 93, 163 | "char": "z", 164 | "width": 44, 165 | "height": 63, 166 | "xoffset": 2, 167 | "yoffset": 2, 168 | "xadvance": 49, 169 | "chnl": 15, 170 | "x": 72, 171 | "y": 148, 172 | "page": 0 173 | }, 174 | { 175 | "id": 121, 176 | "index": 92, 177 | "char": "y", 178 | "width": 44, 179 | "height": 63, 180 | "xoffset": 2, 181 | "yoffset": 2, 182 | "xadvance": 49, 183 | "chnl": 15, 184 | "x": 117, 185 | "y": 128, 186 | "page": 0 187 | }, 188 | { 189 | "id": 120, 190 | "index": 91, 191 | "char": "x", 192 | "width": 45, 193 | "height": 63, 194 | "xoffset": 2, 195 | "yoffset": 2, 196 | "xadvance": 49, 197 | "chnl": 15, 198 | "x": 168, 199 | "y": 0, 200 | "page": 0 201 | }, 202 | { 203 | "id": 47, 204 | "index": 18, 205 | "char": "/", 206 | "width": 36, 207 | "height": 63, 208 | "xoffset": 2, 209 | "yoffset": 2, 210 | "xadvance": 40, 211 | "chnl": 15, 212 | "x": 168, 213 | "y": 64, 214 | "page": 0 215 | }, 216 | { 217 | "id": 48, 218 | "index": 19, 219 | "char": "0", 220 | "width": 44, 221 | "height": 63, 222 | "xoffset": 2, 223 | "yoffset": 2, 224 | "xadvance": 49, 225 | "chnl": 15, 226 | "x": 162, 227 | "y": 128, 228 | "page": 0 229 | }, 230 | { 231 | "id": 49, 232 | "index": 20, 233 | "char": "1", 234 | "width": 15, 235 | "height": 63, 236 | "xoffset": 2, 237 | "yoffset": 2, 238 | "xadvance": 20, 239 | "chnl": 15, 240 | "x": 205, 241 | "y": 64, 242 | "page": 0 243 | }, 244 | { 245 | "id": 50, 246 | "index": 21, 247 | "char": "2", 248 | "width": 44, 249 | "height": 63, 250 | "xoffset": 2, 251 | "yoffset": 2, 252 | "xadvance": 49, 253 | "chnl": 15, 254 | "x": 0, 255 | "y": 212, 256 | "page": 0 257 | }, 258 | { 259 | "id": 51, 260 | "index": 22, 261 | "char": "3", 262 | "width": 44, 263 | "height": 63, 264 | "xoffset": 2, 265 | "yoffset": 2, 266 | "xadvance": 49, 267 | "chnl": 15, 268 | "x": 45, 269 | "y": 212, 270 | "page": 0 271 | }, 272 | { 273 | "id": 52, 274 | "index": 23, 275 | "char": "4", 276 | "width": 44, 277 | "height": 63, 278 | "xoffset": 2, 279 | "yoffset": 2, 280 | "xadvance": 49, 281 | "chnl": 15, 282 | "x": 90, 283 | "y": 212, 284 | "page": 0 285 | }, 286 | { 287 | "id": 53, 288 | "index": 24, 289 | "char": "5", 290 | "width": 44, 291 | "height": 63, 292 | "xoffset": 2, 293 | "yoffset": 2, 294 | "xadvance": 49, 295 | "chnl": 15, 296 | "x": 135, 297 | "y": 192, 298 | "page": 0 299 | }, 300 | { 301 | "id": 54, 302 | "index": 25, 303 | "char": "6", 304 | "width": 44, 305 | "height": 63, 306 | "xoffset": 2, 307 | "yoffset": 2, 308 | "xadvance": 49, 309 | "chnl": 15, 310 | "x": 180, 311 | "y": 192, 312 | "page": 0 313 | }, 314 | { 315 | "id": 55, 316 | "index": 26, 317 | "char": "7", 318 | "width": 44, 319 | "height": 63, 320 | "xoffset": 2, 321 | "yoffset": 2, 322 | "xadvance": 49, 323 | "chnl": 15, 324 | "x": 214, 325 | "y": 0, 326 | "page": 0 327 | }, 328 | { 329 | "id": 56, 330 | "index": 27, 331 | "char": "8", 332 | "width": 44, 333 | "height": 63, 334 | "xoffset": 2, 335 | "yoffset": 2, 336 | "xadvance": 49, 337 | "chnl": 15, 338 | "x": 207, 339 | "y": 128, 340 | "page": 0 341 | }, 342 | { 343 | "id": 57, 344 | "index": 28, 345 | "char": "9", 346 | "width": 44, 347 | "height": 63, 348 | "xoffset": 2, 349 | "yoffset": 2, 350 | "xadvance": 49, 351 | "chnl": 15, 352 | "x": 221, 353 | "y": 64, 354 | "page": 0 355 | }, 356 | { 357 | "id": 116, 358 | "index": 87, 359 | "char": "t", 360 | "width": 44, 361 | "height": 63, 362 | "xoffset": 2, 363 | "yoffset": 2, 364 | "xadvance": 49, 365 | "chnl": 15, 366 | "x": 225, 367 | "y": 192, 368 | "page": 0 369 | }, 370 | { 371 | "id": 115, 372 | "index": 86, 373 | "char": "s", 374 | "width": 44, 375 | "height": 63, 376 | "xoffset": 2, 377 | "yoffset": 2, 378 | "xadvance": 49, 379 | "chnl": 15, 380 | "x": 259, 381 | "y": 0, 382 | "page": 0 383 | }, 384 | { 385 | "id": 114, 386 | "index": 85, 387 | "char": "r", 388 | "width": 44, 389 | "height": 63, 390 | "xoffset": 2, 391 | "yoffset": 2, 392 | "xadvance": 49, 393 | "chnl": 15, 394 | "x": 252, 395 | "y": 128, 396 | "page": 0 397 | }, 398 | { 399 | "id": 79, 400 | "index": 50, 401 | "char": "O", 402 | "width": 44, 403 | "height": 63, 404 | "xoffset": 2, 405 | "yoffset": 2, 406 | "xadvance": 49, 407 | "chnl": 15, 408 | "x": 266, 409 | "y": 64, 410 | "page": 0 411 | }, 412 | { 413 | "id": 112, 414 | "index": 83, 415 | "char": "p", 416 | "width": 44, 417 | "height": 63, 418 | "xoffset": 2, 419 | "yoffset": 2, 420 | "xadvance": 49, 421 | "chnl": 15, 422 | "x": 270, 423 | "y": 192, 424 | "page": 0 425 | }, 426 | { 427 | "id": 63, 428 | "index": 34, 429 | "char": "?", 430 | "width": 44, 431 | "height": 63, 432 | "xoffset": 2, 433 | "yoffset": 2, 434 | "xadvance": 49, 435 | "chnl": 15, 436 | "x": 0, 437 | "y": 276, 438 | "page": 0 439 | }, 440 | { 441 | "id": 64, 442 | "index": 35, 443 | "char": "@", 444 | "width": 44, 445 | "height": 63, 446 | "xoffset": 2, 447 | "yoffset": 2, 448 | "xadvance": 49, 449 | "chnl": 15, 450 | "x": 45, 451 | "y": 276, 452 | "page": 0 453 | }, 454 | { 455 | "id": 65, 456 | "index": 36, 457 | "char": "A", 458 | "width": 44, 459 | "height": 63, 460 | "xoffset": 2, 461 | "yoffset": 2, 462 | "xadvance": 49, 463 | "chnl": 15, 464 | "x": 90, 465 | "y": 276, 466 | "page": 0 467 | }, 468 | { 469 | "id": 66, 470 | "index": 37, 471 | "char": "B", 472 | "width": 44, 473 | "height": 63, 474 | "xoffset": 2, 475 | "yoffset": 2, 476 | "xadvance": 49, 477 | "chnl": 15, 478 | "x": 135, 479 | "y": 256, 480 | "page": 0 481 | }, 482 | { 483 | "id": 67, 484 | "index": 38, 485 | "char": "C", 486 | "width": 44, 487 | "height": 63, 488 | "xoffset": 2, 489 | "yoffset": 2, 490 | "xadvance": 49, 491 | "chnl": 15, 492 | "x": 180, 493 | "y": 256, 494 | "page": 0 495 | }, 496 | { 497 | "id": 68, 498 | "index": 39, 499 | "char": "D", 500 | "width": 44, 501 | "height": 63, 502 | "xoffset": 2, 503 | "yoffset": 2, 504 | "xadvance": 49, 505 | "chnl": 15, 506 | "x": 225, 507 | "y": 256, 508 | "page": 0 509 | }, 510 | { 511 | "id": 69, 512 | "index": 40, 513 | "char": "E", 514 | "width": 44, 515 | "height": 63, 516 | "xoffset": 2, 517 | "yoffset": 2, 518 | "xadvance": 49, 519 | "chnl": 15, 520 | "x": 270, 521 | "y": 256, 522 | "page": 0 523 | }, 524 | { 525 | "id": 70, 526 | "index": 41, 527 | "char": "F", 528 | "width": 44, 529 | "height": 63, 530 | "xoffset": 2, 531 | "yoffset": 2, 532 | "xadvance": 49, 533 | "chnl": 15, 534 | "x": 304, 535 | "y": 0, 536 | "page": 0 537 | }, 538 | { 539 | "id": 71, 540 | "index": 42, 541 | "char": "G", 542 | "width": 44, 543 | "height": 63, 544 | "xoffset": 2, 545 | "yoffset": 2, 546 | "xadvance": 49, 547 | "chnl": 15, 548 | "x": 297, 549 | "y": 128, 550 | "page": 0 551 | }, 552 | { 553 | "id": 72, 554 | "index": 43, 555 | "char": "H", 556 | "width": 44, 557 | "height": 63, 558 | "xoffset": 2, 559 | "yoffset": 2, 560 | "xadvance": 49, 561 | "chnl": 15, 562 | "x": 311, 563 | "y": 64, 564 | "page": 0 565 | }, 566 | { 567 | "id": 73, 568 | "index": 44, 569 | "char": "I", 570 | "width": 15, 571 | "height": 63, 572 | "xoffset": 2, 573 | "yoffset": 2, 574 | "xadvance": 20, 575 | "chnl": 15, 576 | "x": 349, 577 | "y": 0, 578 | "page": 0 579 | }, 580 | { 581 | "id": 74, 582 | "index": 45, 583 | "char": "J", 584 | "width": 44, 585 | "height": 63, 586 | "xoffset": 2, 587 | "yoffset": 2, 588 | "xadvance": 49, 589 | "chnl": 15, 590 | "x": 315, 591 | "y": 192, 592 | "page": 0 593 | }, 594 | { 595 | "id": 75, 596 | "index": 46, 597 | "char": "K", 598 | "width": 44, 599 | "height": 63, 600 | "xoffset": 2, 601 | "yoffset": 2, 602 | "xadvance": 49, 603 | "chnl": 15, 604 | "x": 315, 605 | "y": 256, 606 | "page": 0 607 | }, 608 | { 609 | "id": 125, 610 | "index": 96, 611 | "char": "}", 612 | "width": 26, 613 | "height": 63, 614 | "xoffset": 2, 615 | "yoffset": 2, 616 | "xadvance": 30, 617 | "chnl": 15, 618 | "x": 0, 619 | "y": 340, 620 | "page": 0 621 | }, 622 | { 623 | "id": 77, 624 | "index": 48, 625 | "char": "M", 626 | "width": 49, 627 | "height": 63, 628 | "xoffset": 2, 629 | "yoffset": 2, 630 | "xadvance": 54, 631 | "chnl": 15, 632 | "x": 27, 633 | "y": 340, 634 | "page": 0 635 | }, 636 | { 637 | "id": 78, 638 | "index": 49, 639 | "char": "N", 640 | "width": 44, 641 | "height": 63, 642 | "xoffset": 2, 643 | "yoffset": 2, 644 | "xadvance": 49, 645 | "chnl": 15, 646 | "x": 77, 647 | "y": 340, 648 | "page": 0 649 | }, 650 | { 651 | "id": 33, 652 | "index": 4, 653 | "char": "!", 654 | "width": 15, 655 | "height": 63, 656 | "xoffset": 2, 657 | "yoffset": 2, 658 | "xadvance": 20, 659 | "chnl": 15, 660 | "x": 342, 661 | "y": 128, 662 | "page": 0 663 | }, 664 | { 665 | "id": 80, 666 | "index": 51, 667 | "char": "P", 668 | "width": 44, 669 | "height": 63, 670 | "xoffset": 2, 671 | "yoffset": 2, 672 | "xadvance": 49, 673 | "chnl": 15, 674 | "x": 122, 675 | "y": 340, 676 | "page": 0 677 | }, 678 | { 679 | "id": 37, 680 | "index": 8, 681 | "char": "%", 682 | "width": 60, 683 | "height": 63, 684 | "xoffset": 2, 685 | "yoffset": 2, 686 | "xadvance": 64, 687 | "chnl": 15, 688 | "x": 167, 689 | "y": 320, 690 | "page": 0 691 | }, 692 | { 693 | "id": 82, 694 | "index": 53, 695 | "char": "R", 696 | "width": 44, 697 | "height": 63, 698 | "xoffset": 2, 699 | "yoffset": 2, 700 | "xadvance": 49, 701 | "chnl": 15, 702 | "x": 228, 703 | "y": 320, 704 | "page": 0 705 | }, 706 | { 707 | "id": 83, 708 | "index": 54, 709 | "char": "S", 710 | "width": 44, 711 | "height": 63, 712 | "xoffset": 2, 713 | "yoffset": 2, 714 | "xadvance": 49, 715 | "chnl": 15, 716 | "x": 273, 717 | "y": 320, 718 | "page": 0 719 | }, 720 | { 721 | "id": 84, 722 | "index": 55, 723 | "char": "T", 724 | "width": 44, 725 | "height": 63, 726 | "xoffset": 2, 727 | "yoffset": 2, 728 | "xadvance": 49, 729 | "chnl": 15, 730 | "x": 318, 731 | "y": 320, 732 | "page": 0 733 | }, 734 | { 735 | "id": 85, 736 | "index": 56, 737 | "char": "U", 738 | "width": 44, 739 | "height": 63, 740 | "xoffset": 2, 741 | "yoffset": 2, 742 | "xadvance": 49, 743 | "chnl": 15, 744 | "x": 356, 745 | "y": 64, 746 | "page": 0 747 | }, 748 | { 749 | "id": 86, 750 | "index": 57, 751 | "char": "V", 752 | "width": 44, 753 | "height": 63, 754 | "xoffset": 2, 755 | "yoffset": 2, 756 | "xadvance": 49, 757 | "chnl": 15, 758 | "x": 358, 759 | "y": 128, 760 | "page": 0 761 | }, 762 | { 763 | "id": 87, 764 | "index": 58, 765 | "char": "W", 766 | "width": 61, 767 | "height": 63, 768 | "xoffset": 2, 769 | "yoffset": 2, 770 | "xadvance": 66, 771 | "chnl": 15, 772 | "x": 0, 773 | "y": 404, 774 | "page": 0 775 | }, 776 | { 777 | "id": 88, 778 | "index": 59, 779 | "char": "X", 780 | "width": 45, 781 | "height": 63, 782 | "xoffset": 2, 783 | "yoffset": 2, 784 | "xadvance": 49, 785 | "chnl": 15, 786 | "x": 363, 787 | "y": 192, 788 | "page": 0 789 | }, 790 | { 791 | "id": 89, 792 | "index": 60, 793 | "char": "Y", 794 | "width": 44, 795 | "height": 63, 796 | "xoffset": 2, 797 | "yoffset": 2, 798 | "xadvance": 49, 799 | "chnl": 15, 800 | "x": 365, 801 | "y": 0, 802 | "page": 0 803 | }, 804 | { 805 | "id": 90, 806 | "index": 61, 807 | "char": "Z", 808 | "width": 44, 809 | "height": 63, 810 | "xoffset": 2, 811 | "yoffset": 2, 812 | "xadvance": 49, 813 | "chnl": 15, 814 | "x": 360, 815 | "y": 256, 816 | "page": 0 817 | }, 818 | { 819 | "id": 91, 820 | "index": 62, 821 | "char": "[", 822 | "width": 21, 823 | "height": 63, 824 | "xoffset": 2, 825 | "yoffset": 2, 826 | "xadvance": 25, 827 | "chnl": 15, 828 | "x": 62, 829 | "y": 404, 830 | "page": 0 831 | }, 832 | { 833 | "id": 92, 834 | "index": 63, 835 | "char": "\\", 836 | "width": 36, 837 | "height": 63, 838 | "xoffset": 2, 839 | "yoffset": 2, 840 | "xadvance": 40, 841 | "chnl": 15, 842 | "x": 84, 843 | "y": 404, 844 | "page": 0 845 | }, 846 | { 847 | "id": 93, 848 | "index": 64, 849 | "char": "]", 850 | "width": 21, 851 | "height": 63, 852 | "xoffset": 2, 853 | "yoffset": 2, 854 | "xadvance": 25, 855 | "chnl": 15, 856 | "x": 121, 857 | "y": 404, 858 | "page": 0 859 | }, 860 | { 861 | "id": 111, 862 | "index": 82, 863 | "char": "o", 864 | "width": 44, 865 | "height": 63, 866 | "xoffset": 2, 867 | "yoffset": 2, 868 | "xadvance": 49, 869 | "chnl": 15, 870 | "x": 363, 871 | "y": 320, 872 | "page": 0 873 | }, 874 | { 875 | "id": 110, 876 | "index": 81, 877 | "char": "n", 878 | "width": 44, 879 | "height": 63, 880 | "xoffset": 2, 881 | "yoffset": 2, 882 | "xadvance": 49, 883 | "chnl": 15, 884 | "x": 143, 885 | "y": 404, 886 | "page": 0 887 | }, 888 | { 889 | "id": 119, 890 | "index": 90, 891 | "char": "w", 892 | "width": 61, 893 | "height": 63, 894 | "xoffset": 2, 895 | "yoffset": 2, 896 | "xadvance": 66, 897 | "chnl": 15, 898 | "x": 188, 899 | "y": 384, 900 | "page": 0 901 | }, 902 | { 903 | "id": 97, 904 | "index": 68, 905 | "char": "a", 906 | "width": 44, 907 | "height": 63, 908 | "xoffset": 2, 909 | "yoffset": 2, 910 | "xadvance": 49, 911 | "chnl": 15, 912 | "x": 250, 913 | "y": 384, 914 | "page": 0 915 | }, 916 | { 917 | "id": 98, 918 | "index": 69, 919 | "char": "b", 920 | "width": 44, 921 | "height": 63, 922 | "xoffset": 2, 923 | "yoffset": 2, 924 | "xadvance": 49, 925 | "chnl": 15, 926 | "x": 295, 927 | "y": 384, 928 | "page": 0 929 | }, 930 | { 931 | "id": 99, 932 | "index": 70, 933 | "char": "c", 934 | "width": 44, 935 | "height": 63, 936 | "xoffset": 2, 937 | "yoffset": 2, 938 | "xadvance": 49, 939 | "chnl": 15, 940 | "x": 340, 941 | "y": 384, 942 | "page": 0 943 | }, 944 | { 945 | "id": 100, 946 | "index": 71, 947 | "char": "d", 948 | "width": 44, 949 | "height": 63, 950 | "xoffset": 2, 951 | "yoffset": 2, 952 | "xadvance": 49, 953 | "chnl": 15, 954 | "x": 401, 955 | "y": 64, 956 | "page": 0 957 | }, 958 | { 959 | "id": 101, 960 | "index": 72, 961 | "char": "e", 962 | "width": 44, 963 | "height": 63, 964 | "xoffset": 2, 965 | "yoffset": 2, 966 | "xadvance": 49, 967 | "chnl": 15, 968 | "x": 405, 969 | "y": 256, 970 | "page": 0 971 | }, 972 | { 973 | "id": 102, 974 | "index": 73, 975 | "char": "f", 976 | "width": 44, 977 | "height": 63, 978 | "xoffset": 2, 979 | "yoffset": 2, 980 | "xadvance": 49, 981 | "chnl": 15, 982 | "x": 403, 983 | "y": 128, 984 | "page": 0 985 | }, 986 | { 987 | "id": 103, 988 | "index": 74, 989 | "char": "g", 990 | "width": 44, 991 | "height": 63, 992 | "xoffset": 2, 993 | "yoffset": 2, 994 | "xadvance": 49, 995 | "chnl": 15, 996 | "x": 410, 997 | "y": 0, 998 | "page": 0 999 | }, 1000 | { 1001 | "id": 104, 1002 | "index": 75, 1003 | "char": "h", 1004 | "width": 44, 1005 | "height": 63, 1006 | "xoffset": 2, 1007 | "yoffset": 2, 1008 | "xadvance": 49, 1009 | "chnl": 15, 1010 | "x": 409, 1011 | "y": 192, 1012 | "page": 0 1013 | }, 1014 | { 1015 | "id": 105, 1016 | "index": 76, 1017 | "char": "i", 1018 | "width": 15, 1019 | "height": 63, 1020 | "xoffset": 2, 1021 | "yoffset": 2, 1022 | "xadvance": 20, 1023 | "chnl": 15, 1024 | "x": 385, 1025 | "y": 384, 1026 | "page": 0 1027 | }, 1028 | { 1029 | "id": 106, 1030 | "index": 77, 1031 | "char": "j", 1032 | "width": 44, 1033 | "height": 63, 1034 | "xoffset": 2, 1035 | "yoffset": 2, 1036 | "xadvance": 49, 1037 | "chnl": 15, 1038 | "x": 408, 1039 | "y": 320, 1040 | "page": 0 1041 | }, 1042 | { 1043 | "id": 107, 1044 | "index": 78, 1045 | "char": "k", 1046 | "width": 44, 1047 | "height": 63, 1048 | "xoffset": 2, 1049 | "yoffset": 2, 1050 | "xadvance": 49, 1051 | "chnl": 15, 1052 | "x": 401, 1053 | "y": 384, 1054 | "page": 0 1055 | }, 1056 | { 1057 | "id": 108, 1058 | "index": 79, 1059 | "char": "l", 1060 | "width": 44, 1061 | "height": 63, 1062 | "xoffset": 2, 1063 | "yoffset": 2, 1064 | "xadvance": 49, 1065 | "chnl": 15, 1066 | "x": 446, 1067 | "y": 64, 1068 | "page": 0 1069 | }, 1070 | { 1071 | "id": 109, 1072 | "index": 80, 1073 | "char": "m", 1074 | "width": 49, 1075 | "height": 63, 1076 | "xoffset": 2, 1077 | "yoffset": 2, 1078 | "xadvance": 54, 1079 | "chnl": 15, 1080 | "x": 450, 1081 | "y": 256, 1082 | "page": 0 1083 | }, 1084 | { 1085 | "id": 118, 1086 | "index": 89, 1087 | "char": "v", 1088 | "width": 44, 1089 | "height": 63, 1090 | "xoffset": 2, 1091 | "yoffset": 2, 1092 | "xadvance": 49, 1093 | "chnl": 15, 1094 | "x": 448, 1095 | "y": 128, 1096 | "page": 0 1097 | }, 1098 | { 1099 | "id": 59, 1100 | "index": 30, 1101 | "char": ";", 1102 | "width": 15, 1103 | "height": 52, 1104 | "xoffset": 2, 1105 | "yoffset": 23, 1106 | "xadvance": 20, 1107 | "chnl": 15, 1108 | "x": 455, 1109 | "y": 0, 1110 | "page": 0 1111 | }, 1112 | { 1113 | "id": 94, 1114 | "index": 65, 1115 | "char": "^", 1116 | "width": 45, 1117 | "height": 33, 1118 | "xoffset": 2, 1119 | "yoffset": 2, 1120 | "xadvance": 49, 1121 | "chnl": 15, 1122 | "x": 454, 1123 | "y": 192, 1124 | "page": 0 1125 | }, 1126 | { 1127 | "id": 62, 1128 | "index": 33, 1129 | "char": ">", 1130 | "width": 33, 1131 | "height": 45, 1132 | "xoffset": 2, 1133 | "yoffset": 8, 1134 | "xadvance": 37, 1135 | "chnl": 15, 1136 | "x": 453, 1137 | "y": 320, 1138 | "page": 0 1139 | }, 1140 | { 1141 | "id": 60, 1142 | "index": 31, 1143 | "char": "<", 1144 | "width": 33, 1145 | "height": 45, 1146 | "xoffset": 2, 1147 | "yoffset": 8, 1148 | "xadvance": 37, 1149 | "chnl": 15, 1150 | "x": 453, 1151 | "y": 366, 1152 | "page": 0 1153 | }, 1154 | { 1155 | "id": 95, 1156 | "index": 66, 1157 | "char": "_", 1158 | "width": 42, 1159 | "height": 9, 1160 | "xoffset": 2, 1161 | "yoffset": 61, 1162 | "xadvance": 46, 1163 | "chnl": 15, 1164 | "x": 455, 1165 | "y": 53, 1166 | "page": 0 1167 | }, 1168 | { 1169 | "id": 58, 1170 | "index": 29, 1171 | "char": ":", 1172 | "width": 15, 1173 | "height": 42, 1174 | "xoffset": 2, 1175 | "yoffset": 23, 1176 | "xadvance": 20, 1177 | "chnl": 15, 1178 | "x": 471, 1179 | "y": 0, 1180 | "page": 0 1181 | }, 1182 | { 1183 | "id": 61, 1184 | "index": 32, 1185 | "char": "=", 1186 | "width": 33, 1187 | "height": 31, 1188 | "xoffset": 2, 1189 | "yoffset": 16, 1190 | "xadvance": 37, 1191 | "chnl": 15, 1192 | "x": 446, 1193 | "y": 412, 1194 | "page": 0 1195 | }, 1196 | { 1197 | "id": 43, 1198 | "index": 14, 1199 | "char": "+", 1200 | "width": 33, 1201 | "height": 33, 1202 | "xoffset": 2, 1203 | "yoffset": 18, 1204 | "xadvance": 37, 1205 | "chnl": 15, 1206 | "x": 0, 1207 | "y": 468, 1208 | "page": 0 1209 | }, 1210 | { 1211 | "id": 126, 1212 | "index": 97, 1213 | "char": "~", 1214 | "width": 32, 1215 | "height": 15, 1216 | "xoffset": 2, 1217 | "yoffset": 24, 1218 | "xadvance": 36, 1219 | "chnl": 15, 1220 | "x": 454, 1221 | "y": 226, 1222 | "page": 0 1223 | }, 1224 | { 1225 | "id": 45, 1226 | "index": 16, 1227 | "char": "-", 1228 | "width": 25, 1229 | "height": 14, 1230 | "xoffset": 2, 1231 | "yoffset": 27, 1232 | "xadvance": 30, 1233 | "chnl": 15, 1234 | "x": 135, 1235 | "y": 320, 1236 | "page": 0 1237 | }, 1238 | { 1239 | "id": 44, 1240 | "index": 15, 1241 | "char": ",", 1242 | "width": 15, 1243 | "height": 24, 1244 | "xoffset": 2, 1245 | "yoffset": 50, 1246 | "xadvance": 20, 1247 | "chnl": 15, 1248 | "x": 480, 1249 | "y": 412, 1250 | "page": 0 1251 | }, 1252 | { 1253 | "id": 34, 1254 | "index": 5, 1255 | "char": "\"", 1256 | "width": 24, 1257 | "height": 20, 1258 | "xoffset": 2, 1259 | "yoffset": 2, 1260 | "xadvance": 29, 1261 | "chnl": 15, 1262 | "x": 34, 1263 | "y": 468, 1264 | "page": 0 1265 | }, 1266 | { 1267 | "id": 42, 1268 | "index": 13, 1269 | "char": "*", 1270 | "width": 20, 1271 | "height": 20, 1272 | "xoffset": 2, 1273 | "yoffset": 2, 1274 | "xadvance": 24, 1275 | "chnl": 15, 1276 | "x": 59, 1277 | "y": 468, 1278 | "page": 0 1279 | }, 1280 | { 1281 | "id": 39, 1282 | "index": 10, 1283 | "char": "'", 1284 | "width": 12, 1285 | "height": 20, 1286 | "xoffset": 2, 1287 | "yoffset": 2, 1288 | "xadvance": 16, 1289 | "chnl": 15, 1290 | "x": 487, 1291 | "y": 0, 1292 | "page": 0 1293 | }, 1294 | { 1295 | "id": 46, 1296 | "index": 17, 1297 | "char": ".", 1298 | "width": 15, 1299 | "height": 15, 1300 | "xoffset": 2, 1301 | "yoffset": 50, 1302 | "xadvance": 20, 1303 | "chnl": 15, 1304 | "x": 101, 1305 | "y": 128, 1306 | "page": 0 1307 | }, 1308 | { 1309 | "id": 96, 1310 | "index": 67, 1311 | "char": "`", 1312 | "width": 14, 1313 | "height": 13, 1314 | "xoffset": 2, 1315 | "yoffset": -8, 1316 | "xadvance": 19, 1317 | "chnl": 15, 1318 | "x": 454, 1319 | "y": 242, 1320 | "page": 0 1321 | }, 1322 | { 1323 | "id": 32, 1324 | "index": 3, 1325 | "char": " ", 1326 | "width": 0, 1327 | "height": 0, 1328 | "xoffset": -2, 1329 | "yoffset": 61, 1330 | "xadvance": 21, 1331 | "chnl": 15, 1332 | "x": 455, 1333 | "y": 63, 1334 | "page": 0 1335 | } 1336 | ], 1337 | "info": { 1338 | "face": "Square", 1339 | "size": 84, 1340 | "bold": 0, 1341 | "italic": 0, 1342 | "charset": [ 1343 | " ", 1344 | "!", 1345 | "\"", 1346 | "#", 1347 | "$", 1348 | "%", 1349 | "&", 1350 | "'", 1351 | "(", 1352 | ")", 1353 | "*", 1354 | "+", 1355 | ",", 1356 | "-", 1357 | ".", 1358 | "/", 1359 | "0", 1360 | "1", 1361 | "2", 1362 | "3", 1363 | "4", 1364 | "5", 1365 | "6", 1366 | "7", 1367 | "8", 1368 | "9", 1369 | ":", 1370 | ";", 1371 | "<", 1372 | "=", 1373 | ">", 1374 | "?", 1375 | "@", 1376 | "A", 1377 | "B", 1378 | "C", 1379 | "D", 1380 | "E", 1381 | "F", 1382 | "G", 1383 | "H", 1384 | "I", 1385 | "J", 1386 | "K", 1387 | "L", 1388 | "M", 1389 | "N", 1390 | "O", 1391 | "P", 1392 | "Q", 1393 | "R", 1394 | "S", 1395 | "T", 1396 | "U", 1397 | "V", 1398 | "W", 1399 | "X", 1400 | "Y", 1401 | "Z", 1402 | "[", 1403 | "\\", 1404 | "]", 1405 | "^", 1406 | "_", 1407 | "`", 1408 | "a", 1409 | "b", 1410 | "c", 1411 | "d", 1412 | "e", 1413 | "f", 1414 | "g", 1415 | "h", 1416 | "i", 1417 | "j", 1418 | "k", 1419 | "l", 1420 | "m", 1421 | "n", 1422 | "o", 1423 | "p", 1424 | "q", 1425 | "r", 1426 | "s", 1427 | "t", 1428 | "u", 1429 | "v", 1430 | "w", 1431 | "x", 1432 | "y", 1433 | "z", 1434 | "{", 1435 | "|", 1436 | "}", 1437 | "~" 1438 | ], 1439 | "unicode": 1, 1440 | "stretchH": 100, 1441 | "smooth": 1, 1442 | "aa": 1, 1443 | "padding": [ 1444 | 0, 1445 | 0, 1446 | 0, 1447 | 0 1448 | ], 1449 | "spacing": [ 1450 | 0, 1451 | 0 1452 | ] 1453 | }, 1454 | "common": { 1455 | "lineHeight": 84, 1456 | "base": 61, 1457 | "scaleW": 499, 1458 | "scaleH": 506, 1459 | "pages": 1, 1460 | "packed": 0, 1461 | "alphaChnl": 0, 1462 | "redChnl": 0, 1463 | "greenChnl": 0, 1464 | "blueChnl": 0 1465 | }, 1466 | "distanceField": { 1467 | "fieldType": "msdf", 1468 | "distanceRange": 4 1469 | }, 1470 | "kernings": [ 1471 | { 1472 | "first": 70, 1473 | "second": 44, 1474 | "amount": -23 1475 | }, 1476 | { 1477 | "first": 70, 1478 | "second": 46, 1479 | "amount": -23 1480 | }, 1481 | { 1482 | "first": 76, 1483 | "second": 84, 1484 | "amount": -10 1485 | }, 1486 | { 1487 | "first": 76, 1488 | "second": 86, 1489 | "amount": -6 1490 | }, 1491 | { 1492 | "first": 76, 1493 | "second": 87, 1494 | "amount": -3 1495 | }, 1496 | { 1497 | "first": 76, 1498 | "second": 89, 1499 | "amount": -8 1500 | }, 1501 | { 1502 | "first": 76, 1503 | "second": 121, 1504 | "amount": -8 1505 | }, 1506 | { 1507 | "first": 80, 1508 | "second": 44, 1509 | "amount": -23 1510 | }, 1511 | { 1512 | "first": 80, 1513 | "second": 46, 1514 | "amount": -23 1515 | }, 1516 | { 1517 | "first": 84, 1518 | "second": 44, 1519 | "amount": -7 1520 | }, 1521 | { 1522 | "first": 84, 1523 | "second": 45, 1524 | "amount": -7 1525 | }, 1526 | { 1527 | "first": 84, 1528 | "second": 46, 1529 | "amount": -7 1530 | }, 1531 | { 1532 | "first": 84, 1533 | "second": 58, 1534 | "amount": -7 1535 | }, 1536 | { 1537 | "first": 84, 1538 | "second": 59, 1539 | "amount": -7 1540 | }, 1541 | { 1542 | "first": 86, 1543 | "second": 44, 1544 | "amount": -3 1545 | }, 1546 | { 1547 | "first": 86, 1548 | "second": 46, 1549 | "amount": -3 1550 | }, 1551 | { 1552 | "first": 87, 1553 | "second": 44, 1554 | "amount": -2 1555 | }, 1556 | { 1557 | "first": 87, 1558 | "second": 46, 1559 | "amount": -2 1560 | }, 1561 | { 1562 | "first": 89, 1563 | "second": 44, 1564 | "amount": -8 1565 | }, 1566 | { 1567 | "first": 89, 1568 | "second": 45, 1569 | "amount": -6 1570 | }, 1571 | { 1572 | "first": 89, 1573 | "second": 46, 1574 | "amount": -8 1575 | }, 1576 | { 1577 | "first": 89, 1578 | "second": 58, 1579 | "amount": -5 1580 | }, 1581 | { 1582 | "first": 89, 1583 | "second": 59, 1584 | "amount": -5 1585 | }, 1586 | { 1587 | "first": 118, 1588 | "second": 44, 1589 | "amount": -3 1590 | }, 1591 | { 1592 | "first": 118, 1593 | "second": 46, 1594 | "amount": -3 1595 | }, 1596 | { 1597 | "first": 119, 1598 | "second": 44, 1599 | "amount": -2 1600 | }, 1601 | { 1602 | "first": 119, 1603 | "second": 46, 1604 | "amount": -2 1605 | }, 1606 | { 1607 | "first": 121, 1608 | "second": 44, 1609 | "amount": -8 1610 | }, 1611 | { 1612 | "first": 121, 1613 | "second": 46, 1614 | "amount": -8 1615 | } 1616 | ] 1617 | } --------------------------------------------------------------------------------