├── .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 | }
--------------------------------------------------------------------------------