init dump
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
|
||||
Package.resolved
|
||||
*.blend1
|
41
Package.swift
Normal file
@ -0,0 +1,41 @@
|
||||
// swift-tools-version: 5.9
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "CavesOfJolk-Swift",
|
||||
platforms: [
|
||||
.macOS(.v11)
|
||||
],
|
||||
products: [
|
||||
.executable(name: "Test", targets: ["Test"]),
|
||||
.library(name: "JolkEngine", targets: ["JolkEngine"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: "https://github.com/ctreffs/SwiftSDL2.git",
|
||||
.upToNextMajor(from: "1.4.1")),
|
||||
.package(
|
||||
url: "https://github.com/apple/swift-collections.git",
|
||||
.upToNextMinor(from: "1.1.0"))
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "Test",
|
||||
dependencies: [ .target(name: "JolkEngine") ],
|
||||
resources: [ .process("Resources") ]
|
||||
),
|
||||
.target(
|
||||
name: "JolkEngine",
|
||||
dependencies: [
|
||||
.product(name: "SDL", package: "SwiftSDL2"),
|
||||
.product(name: "Collections", package: "swift-collections"),
|
||||
"HSLuv"
|
||||
],
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-Xcc", "-DGL_SILENCE_DEPRECATION"])
|
||||
]
|
||||
),
|
||||
.target(name: "HSLuv"),
|
||||
]
|
||||
)
|
19
Sources/HSLuv/LICENSE.txt
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015 Alexei Boronine
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
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.
|
40
Sources/HSLuv/README.md
Normal file
@ -0,0 +1,40 @@
|
||||
[](https://cocoapods.org/pods/hsluv-objc)
|
||||
[](https://travis-ci.org/hsluv/hsluv-objc)
|
||||
|
||||
#hsluv-objc
|
||||
|
||||
Objective-C port of [HSLuv](http://www.hsluv.org).
|
||||
|
||||
##Which files are needed?
|
||||
|
||||
If you're using [CocoaPods](https://cocoapods.org) just add `pod 'hsluv-objc'` to your Podfile.
|
||||
|
||||
Otherwise, include this files in your project:
|
||||
|
||||
- hsluv-objc.h
|
||||
- hsluv-objc+Tests.h
|
||||
- hsluv-objc.c
|
||||
|
||||
##How to use
|
||||
|
||||
Import `hsluv-objc.h`, which defines the following functions:
|
||||
|
||||
~~~objective-c
|
||||
// Accepts red, green and blue values between 0 and 1, returns the color in hex format, as in "#012C4A"
|
||||
NSString *rgbToHex(CGFloat red, CGFloat green, CGFloat blue);
|
||||
|
||||
// Accepts an hex color, as in "#012C4A", and stores its red, green and blue components with values between 0 and 1.
|
||||
BOOL hexToRgb(NSString *hex, CGFloat *red, CGFloat *green, CGFloat *blue);
|
||||
|
||||
// Hue is a value between 0 and 360, saturation and lightness between 0 and 100. Stores the RGB in values between 0 and 1.
|
||||
void hsluvToRgb(CGFloat hue, CGFloat saturation, CGFloat lightness, CGFloat *red, CGFloat *green, CGFloat *blue);
|
||||
|
||||
// Red, green and blue values between 0 and 1, stores the hsluv components with hue between 0 and 360, saturation and lightness between 0 and 100.
|
||||
void rgbToHsluv(CGFloat red, CGFloat green, CGFloat blue, CGFloat *hue, CGFloat *saturation, CGFloat *lightness);
|
||||
|
||||
// Hue is a value between 0 and 360, saturation and lightness between 0 and 100. Stores the RGB in values between 0 and 1.
|
||||
void hpluvToRgb(CGFloat hue, CGFloat saturation, CGFloat lightness, CGFloat *red, CGFloat *green, CGFloat *blue);
|
||||
|
||||
// Red, green and blue values between 0 and 1, stores the hpluv components with hue between 0 and 360, saturation and lightness between 0 and 100.
|
||||
void rgbToHpluv(CGFloat red, CGFloat green, CGFloat blue, CGFloat *hue, CGFloat *saturation, CGFloat *lightness);
|
||||
~~~
|
23
Sources/HSLuv/hsluv-objc+Test.h
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// hsluv_objc+Test.h
|
||||
// hsluv-objc
|
||||
//
|
||||
// Created by Roger Tallada on 4/6/15.
|
||||
// Copyright (c) 2015 Alexei Boronine
|
||||
//
|
||||
|
||||
#ifndef hsluv_objc_hsluv_objc_Test_h
|
||||
#define hsluv_objc_hsluv_objc_Test_h
|
||||
|
||||
// Exposed for testing purposes only:
|
||||
typedef struct tuple {
|
||||
CGFloat a, b, c;
|
||||
} Tuple;
|
||||
|
||||
Tuple rgbToXyz(Tuple rgb);
|
||||
Tuple xyzToLuv(Tuple xyz);
|
||||
Tuple luvToLch(Tuple luv);
|
||||
Tuple lchToHsluv(Tuple lch);
|
||||
Tuple lchToHpluv(Tuple lch);
|
||||
|
||||
#endif
|
510
Sources/HSLuv/hsluv-objc.m
Normal file
@ -0,0 +1,510 @@
|
||||
//
|
||||
// hsluv_objc.m
|
||||
// hsluv-objc
|
||||
//
|
||||
// Created by Roger Tallada on 4/6/15.
|
||||
// Copyright (c) 2015 Alexei Boronine
|
||||
//
|
||||
// Implementation of hsluv translated from hsluv.coffee
|
||||
|
||||
|
||||
#import <tgmath.h>
|
||||
#import "hsluv-objc.h"
|
||||
#import "hsluv-objc+Test.h"
|
||||
|
||||
#pragma mark Private funcions
|
||||
|
||||
/*
|
||||
# The math for most of this module was taken from:
|
||||
#
|
||||
# * http://www.easyrgb.com
|
||||
# * http://www.brucelindbloom.com
|
||||
# * Wikipedia
|
||||
#
|
||||
|
||||
|
||||
# All numbers taken from math/bounds.wxm wxMaxima file:
|
||||
#
|
||||
# fpprintprec: 16;
|
||||
# CGFloat(M_XYZ_RGB);
|
||||
# CGFloat(M_RGB_XYZ);
|
||||
# CGFloat(refX);
|
||||
# CGFloat(refY);
|
||||
# CGFloat(refZ);
|
||||
# CGFloat(refU);
|
||||
# CGFloat(refV);
|
||||
# CGFloat(lab_k);
|
||||
# CGFloat(lab_e);
|
||||
#*/
|
||||
|
||||
|
||||
typedef struct tuple2 {
|
||||
CGFloat a, b;
|
||||
} Tuple2;
|
||||
|
||||
static Tuple m[3] = {
|
||||
{ 3.2409699419045214, -1.5373831775700935, -0.49861076029300328}, // R
|
||||
{-0.96924363628087983, 1.8759675015077207, 0.041555057407175613}, // G
|
||||
{ 0.055630079696993609, -0.20397695888897657, 1.0569715142428786} // B
|
||||
};
|
||||
|
||||
static Tuple m_inv[3] = {
|
||||
{0.41239079926595948, 0.35758433938387796, 0.18048078840183429}, // X
|
||||
{0.21263900587151036, 0.71516867876775593, 0.072192315360733715}, // Y
|
||||
{0.019330818715591851, 0.11919477979462599, 0.95053215224966058} // Z
|
||||
};
|
||||
|
||||
//Constants
|
||||
CGFloat refU = 0.19783000664283681;
|
||||
CGFloat refV = 0.468319994938791;
|
||||
|
||||
// CIE LUV constants
|
||||
CGFloat kappa = 903.2962962962963;
|
||||
CGFloat epsilon = 0.0088564516790356308;
|
||||
|
||||
// For a given lightness, return a list of 6 lines in slope-intercept
|
||||
// form that represent the bounds in CIELUV, stepping over which will
|
||||
// push a value out of the RGB gamut
|
||||
Tuple2 * getBounds(CGFloat l) {
|
||||
CGFloat sub1 = pow(l + 16, 3) / 1560896;
|
||||
CGFloat sub2 = sub1 > epsilon ? sub1 : (l / kappa);
|
||||
|
||||
Tuple2 *ret = malloc(6 * sizeof(Tuple2));
|
||||
|
||||
for (int channel=0; channel<3; channel++) {
|
||||
Tuple mTuple = m[channel];
|
||||
|
||||
CGFloat m1 = mTuple.a;
|
||||
CGFloat m2 = mTuple.b;
|
||||
CGFloat m3 = mTuple.c;
|
||||
|
||||
for (int t=0; t <= 1; t++) {
|
||||
CGFloat top1 = (284517 * m1 - 94839 * m3) * sub2;
|
||||
CGFloat top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * l * sub2 - 769860 * t * l;
|
||||
CGFloat bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t;
|
||||
|
||||
Tuple2 tuple = {top1 / bottom, top2 / bottom};
|
||||
|
||||
NSUInteger lineNumber = channel * 2 + t;
|
||||
ret[lineNumber] = tuple;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
CGFloat intersectLineLine(Tuple2 line1, Tuple2 line2) {
|
||||
return (line1.b - line2.b) / (line2.a - line1.a);
|
||||
}
|
||||
|
||||
CGFloat distanceFromPole(CGPoint point) {
|
||||
return sqrt(pow(point.x, 2) + pow(point.y, 2));
|
||||
}
|
||||
|
||||
CGFloat lengthOfRayUntilIntersect(CGFloat theta, Tuple2 line) {
|
||||
// theta -- angle of ray starting at (0, 0)
|
||||
// m, b -- slope and intercept of line
|
||||
// x1, y1 -- coordinates of intersection
|
||||
// len -- length of ray until it intersects with line
|
||||
//
|
||||
// b + m * x1 = y1
|
||||
// len >= 0
|
||||
// len * cos(theta) = x1
|
||||
// len * sin(theta) = y1
|
||||
//
|
||||
//
|
||||
// b + m * (len * cos(theta)) = len * sin(theta)
|
||||
// b = len * sin(hrad) - m * len * cos(theta)
|
||||
// b = len * (sin(hrad) - m * cos(hrad))
|
||||
// len = b / (sin(hrad) - m * cos(hrad))
|
||||
//
|
||||
CGFloat m1 = line.a;
|
||||
CGFloat b1 = line.b;
|
||||
CGFloat len = b1 / (sin(theta) - m1 * cos(theta));
|
||||
// if (len < 0) {
|
||||
// return 0;
|
||||
// }
|
||||
return len;
|
||||
}
|
||||
|
||||
// For given lightness, returns the maximum chroma. Keeping the chroma value
|
||||
// below this number will ensure that for any hue, the color is within the RGB
|
||||
// gamut.
|
||||
CGFloat maxSafeChromaForL(CGFloat l) {
|
||||
CGFloat minLength = CGFLOAT_MAX;
|
||||
Tuple2 *bounds = getBounds(l);
|
||||
for (NSUInteger i = 0; i < 6; i++) {
|
||||
Tuple2 boundTuple = bounds[i];
|
||||
|
||||
CGFloat m1 = boundTuple.a;
|
||||
CGFloat b1 = boundTuple.b;
|
||||
|
||||
// x where line intersects with perpendicular running though (0, 0)
|
||||
Tuple2 line2 = {-1 / m1, 0};
|
||||
CGFloat x = intersectLineLine(boundTuple, line2);
|
||||
CGFloat distance = distanceFromPole(CGPointMake(x, b1 + x * m1));
|
||||
if (distance >= 0) {
|
||||
if (distance < minLength) {
|
||||
minLength = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(bounds);
|
||||
return minLength;
|
||||
}
|
||||
|
||||
// For a given lightness and hue, return the maximum chroma that fits in
|
||||
// the RGB gamut.
|
||||
CGFloat maxChromaForLH(CGFloat l, CGFloat h) {
|
||||
CGFloat hrad = h / 360 * M_PI * 2;
|
||||
CGFloat minLength = CGFLOAT_MAX;
|
||||
Tuple2 *bounds = getBounds(l);
|
||||
for (NSUInteger i = 0; i < 6; i++) {
|
||||
Tuple2 lineTuple = bounds[i];
|
||||
CGFloat l = lengthOfRayUntilIntersect(hrad, lineTuple);
|
||||
if (l >= 0) {
|
||||
if (l < minLength) {
|
||||
minLength = l;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(bounds);
|
||||
return minLength;
|
||||
}
|
||||
|
||||
|
||||
|
||||
CGFloat tupleDotProduct(Tuple t1, Tuple t2) {
|
||||
CGFloat ret = 0.0;
|
||||
for (NSUInteger i = 0; i < 3; i++) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
ret += t1.a * t2.a;
|
||||
break;
|
||||
case 1:
|
||||
ret += t1.b * t2.b;
|
||||
break;
|
||||
case 2:
|
||||
ret += t1.c * t2.c;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Used for rgb conversions
|
||||
CGFloat fromLinear(CGFloat c) {
|
||||
if (c <= 0.0031308) {
|
||||
return 12.92 * c;
|
||||
}
|
||||
else {
|
||||
return 1.055 * pow(c, 1 / 2.4) - 0.055;
|
||||
}
|
||||
}
|
||||
|
||||
CGFloat toLinear(CGFloat c) {
|
||||
CGFloat a = 0.055;
|
||||
if (c > 0.04045) {
|
||||
return pow((c + a) / (1 + a), 2.4);
|
||||
}
|
||||
else {
|
||||
return c / 12.92;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Conversion functions
|
||||
|
||||
Tuple xyzToRgb(Tuple xyz) {
|
||||
CGFloat r = fromLinear(tupleDotProduct(m[0], xyz));
|
||||
CGFloat g = fromLinear(tupleDotProduct(m[1], xyz));
|
||||
CGFloat b = fromLinear(tupleDotProduct(m[2], xyz));
|
||||
|
||||
Tuple rgb = {r, g, b};
|
||||
return rgb;
|
||||
}
|
||||
|
||||
Tuple rgbToXyz(Tuple rgb) {
|
||||
Tuple rgbl = {toLinear(rgb.a), toLinear(rgb.b), toLinear(rgb.c)};
|
||||
|
||||
CGFloat x = tupleDotProduct(m_inv[0], rgbl);
|
||||
CGFloat y = tupleDotProduct(m_inv[1], rgbl);
|
||||
CGFloat z = tupleDotProduct(m_inv[2], rgbl);
|
||||
|
||||
Tuple xyz = {x, y, z};
|
||||
return xyz;
|
||||
}
|
||||
|
||||
// http://en.wikipedia.org/wiki/CIELUV
|
||||
// In these formulas, Yn refers to the reference white point. We are using
|
||||
// illuminant D65, so Yn (see refY in Maxima file) equals 1. The formula is
|
||||
// simplified accordingly.
|
||||
CGFloat yToL (CGFloat y) {
|
||||
CGFloat l;
|
||||
if (y <= epsilon) {
|
||||
l = y * kappa;
|
||||
}
|
||||
else {
|
||||
l = 116.0 * pow(y, 1.0/3.0) - 16.0;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
CGFloat lToY (CGFloat l) {
|
||||
if (l <= 8) {
|
||||
return l / kappa;
|
||||
}
|
||||
else {
|
||||
return powl((l + 16) / 116, 3);
|
||||
}
|
||||
}
|
||||
|
||||
Tuple xyzToLuv(Tuple xyz) {
|
||||
CGFloat varU = (4 * xyz.a) / (xyz.a + (15 * xyz.b) + (3 * xyz.c));
|
||||
CGFloat varV = (9 * xyz.b) / (xyz.a + (15 * xyz.b) + (3 * xyz.c));
|
||||
CGFloat l = yToL(xyz.b);
|
||||
// Black will create a divide-by-zero error
|
||||
if (l==0) {
|
||||
Tuple luv = {0, 0, 0};
|
||||
return luv;
|
||||
}
|
||||
CGFloat u = 13 * l * (varU - refU);
|
||||
CGFloat v = 13 * l * (varV - refV);
|
||||
Tuple luv = {l, u, v};
|
||||
return luv;
|
||||
}
|
||||
|
||||
Tuple luvToXyz(Tuple luv) {
|
||||
// Black will create a divide-by-zero error
|
||||
if (luv.a == 0) {
|
||||
Tuple xyz = {0, 0, 0};
|
||||
return xyz;
|
||||
}
|
||||
CGFloat varU = luv.b / (13 * luv.a) + refU;
|
||||
CGFloat varV = luv.c / (13 * luv.a) + refV;
|
||||
CGFloat y = lToY(luv.a);
|
||||
CGFloat x = 0 - (9 * y * varU) / ((varU - 4) * varV - varU * varV);
|
||||
CGFloat z = (9 * y - (15 * varV * y) - (varV * x)) / (3 * varV);
|
||||
Tuple xyz = {x, y, z};
|
||||
return xyz;
|
||||
}
|
||||
|
||||
Tuple luvToLch(Tuple luv) {
|
||||
CGFloat l = luv.a, u = luv.b, v = luv.c;
|
||||
CGFloat h, c = sqrt(pow(u, 2) + pow(v, 2));
|
||||
|
||||
// Greys: disambiguate hue
|
||||
if (c < 0.00000001) {
|
||||
h = 0;
|
||||
}
|
||||
else {
|
||||
CGFloat hrad = atan2(v, u);
|
||||
h = hrad * 360 / 2 / M_PI;
|
||||
if (h < 0) {
|
||||
h = 360 + h;
|
||||
}
|
||||
}
|
||||
|
||||
Tuple lch = {l, c, h};
|
||||
return lch;
|
||||
}
|
||||
|
||||
Tuple lchToLuv(Tuple lch) {
|
||||
CGFloat hRad = lch.c / 360 * 2 * M_PI;
|
||||
CGFloat u = cos(hRad) * lch.b;
|
||||
CGFloat v = sin(hRad) * lch.b;
|
||||
Tuple luv = {lch.a, u, v};
|
||||
return luv;
|
||||
}
|
||||
|
||||
CGFloat checkBorders(CGFloat channel) {
|
||||
if (channel < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (channel > 1) {
|
||||
return 1;
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
BOOL hexToInt(NSString *hex, unsigned int *result) {
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hex];
|
||||
return [scanner scanHexInt:result];
|
||||
}
|
||||
|
||||
#pragma mark hsluv
|
||||
Tuple hsluvToLch(Tuple hsluv) {
|
||||
CGFloat h = hsluv.a, s = hsluv.b, l = hsluv.c, c;
|
||||
|
||||
// White and black: disambiguate chroma
|
||||
if (l > 99.9999999 || l < 0.00000001) {
|
||||
c = 0;
|
||||
}
|
||||
else {
|
||||
CGFloat max = maxChromaForLH(l, h);
|
||||
c = max / 100 * s;
|
||||
}
|
||||
// Greys: disambiguate hue
|
||||
if (s < 0.00000001) {
|
||||
h = 0;
|
||||
}
|
||||
Tuple lch = {l, c, h};
|
||||
return lch;
|
||||
}
|
||||
|
||||
Tuple lchToHsluv(Tuple lch) {
|
||||
CGFloat l = lch.a, c = lch.b, h = lch.c, s;
|
||||
|
||||
// White and black: disambiguate saturation
|
||||
if (l > 99.9999999 || l < 0.00000001) {
|
||||
s = 0;
|
||||
}
|
||||
else {
|
||||
CGFloat max = maxChromaForLH(l, h);
|
||||
s = c / max * 100;
|
||||
}
|
||||
// Greys: disambiguate hue
|
||||
if (c < 0.00000001) {
|
||||
h = 0;
|
||||
}
|
||||
Tuple hsluv = {h, s, l};
|
||||
return hsluv;
|
||||
}
|
||||
|
||||
#pragma mark hsluvP
|
||||
Tuple hpluvToLch(Tuple hpluv) {
|
||||
CGFloat h = hpluv.a, s = hpluv.b, l = hpluv.c, c;
|
||||
|
||||
// White and black: disambiguate chroma
|
||||
if (l > 99.9999999 || l < 0.00000001) {
|
||||
c = 0;
|
||||
}
|
||||
else {
|
||||
CGFloat max = maxSafeChromaForL(l);
|
||||
c = max / 100 * s;
|
||||
}
|
||||
|
||||
// Greys: disambiguate hue
|
||||
if (s < 0.00000001) {
|
||||
h = 0;
|
||||
}
|
||||
Tuple lch = {l, c, h};
|
||||
return lch;
|
||||
}
|
||||
|
||||
Tuple lchToHpluv(Tuple lch) {
|
||||
CGFloat l = lch.a, c = lch.b, h = lch.c, s;
|
||||
|
||||
// White and black: disambiguate saturation
|
||||
if (l > 99.9999999 || l < 0.00000001) {
|
||||
s = 0;
|
||||
}
|
||||
else {
|
||||
CGFloat max = maxSafeChromaForL(l);
|
||||
s = c / max * 100;
|
||||
}
|
||||
// Greys: disambiguate hue
|
||||
if (c < 0.00000001) {
|
||||
h = 0;
|
||||
}
|
||||
|
||||
Tuple hpluv = {h, s, l};
|
||||
return hpluv;
|
||||
}
|
||||
|
||||
CGFloat roundTo6decimals(CGFloat channel) {
|
||||
CGFloat ch = round(channel * 1e6) / 1e6;
|
||||
if (ch < 0 || ch > 1) {
|
||||
@throw [NSString stringWithFormat:@"Illegal rgb value: %@", @(ch)];
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
#pragma mark Public functions
|
||||
|
||||
NSString *rgbToHex(CGFloat red, CGFloat green, CGFloat blue) {
|
||||
NSString *hex = @"#";
|
||||
|
||||
CGFloat r = roundTo6decimals(red);
|
||||
CGFloat g = roundTo6decimals(green);
|
||||
CGFloat b = roundTo6decimals(blue);
|
||||
|
||||
NSString *R = [NSString stringWithFormat:@"%02X", (int)round(r * 255)];
|
||||
NSString *G = [NSString stringWithFormat:@"%02X", (int)round(g * 255)];
|
||||
NSString *B = [NSString stringWithFormat:@"%02X", (int)round(b * 255)];
|
||||
|
||||
return [[[hex stringByAppendingString:R] stringByAppendingString:G] stringByAppendingString:B];
|
||||
}
|
||||
|
||||
BOOL hexToRgb(NSString *hex, CGFloat *red, CGFloat *green, CGFloat *blue) {
|
||||
if ([hex length] >= 7) {
|
||||
if ([hex characterAtIndex:0] == '#') {
|
||||
hex = [hex substringFromIndex:1];
|
||||
}
|
||||
unsigned int r, g, b;
|
||||
|
||||
NSString *redS = [hex substringToIndex:2];
|
||||
if (!hexToInt(redS, &r)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSRange gRange = {2, 2};
|
||||
NSString *greenS = [hex substringWithRange:gRange];
|
||||
if (!hexToInt(greenS, &g)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSRange bRange = {4, 2};
|
||||
NSString *blueS = [hex substringWithRange:bRange];
|
||||
if (!hexToInt(blueS, &b)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
*red = (CGFloat)r / 255;
|
||||
*green = (CGFloat)g / 255;
|
||||
*blue = (CGFloat)b / 255;
|
||||
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
void hsluvToRgb(CGFloat hue, CGFloat saturation, CGFloat lightness, CGFloat *red, CGFloat *green, CGFloat *blue) {
|
||||
Tuple hsluv = {hue, saturation, lightness};
|
||||
|
||||
Tuple rgb = xyzToRgb(luvToXyz(lchToLuv(hsluvToLch(hsluv))));
|
||||
|
||||
*red = rgb.a;
|
||||
*green = rgb.b;
|
||||
*blue = rgb.c;
|
||||
}
|
||||
|
||||
void rgbToHsluv(CGFloat red, CGFloat green, CGFloat blue, CGFloat *hue, CGFloat *saturation, CGFloat *lightness) {
|
||||
Tuple rgb = {red, green, blue};
|
||||
|
||||
Tuple hsluv = lchToHsluv(luvToLch(xyzToLuv(rgbToXyz(rgb))));
|
||||
|
||||
*hue = hsluv.a;
|
||||
*saturation = hsluv.b;
|
||||
*lightness = hsluv.c;
|
||||
}
|
||||
|
||||
void hpluvToRgb(CGFloat hue, CGFloat saturation, CGFloat lightness, CGFloat *red, CGFloat *green, CGFloat *blue) {
|
||||
Tuple hpluv = {hue, saturation, lightness};
|
||||
|
||||
Tuple rgb = xyzToRgb(luvToXyz(lchToLuv(hpluvToLch(hpluv))));
|
||||
|
||||
*red = rgb.a;
|
||||
*green = rgb.b;
|
||||
*blue = rgb.c;
|
||||
}
|
||||
|
||||
void rgbToHpluv(CGFloat red, CGFloat green, CGFloat blue, CGFloat *hue, CGFloat *saturation, CGFloat *lightness) {
|
||||
Tuple rgb = {red, green, blue};
|
||||
|
||||
Tuple hpluv = lchToHpluv(luvToLch(xyzToLuv(rgbToXyz(rgb))));
|
||||
|
||||
*hue = hpluv.a;
|
||||
*saturation = hpluv.b;
|
||||
*lightness = hpluv.c;
|
||||
}
|
33
Sources/HSLuv/include/hsluv-objc.h
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// hsluv_objc.h
|
||||
// hsluv-objc
|
||||
//
|
||||
// Created by Roger Tallada on 4/6/15.
|
||||
// Copyright (c) 2015 Alexei Boronine
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
#ifndef hsluv_objc_h
|
||||
#define hsluv_objc_h
|
||||
|
||||
// Accepts red, green and blue values between 0 and 1, returns the color in hex format, as in "#012C4A"
|
||||
NSString *rgbToHex(CGFloat red, CGFloat green, CGFloat blue);
|
||||
|
||||
// Accepts an hex color, as in "#012C4A", and stores its red, green and blue components with values between 0 and 1.
|
||||
BOOL hexToRgb(NSString *hex, CGFloat *red, CGFloat *green, CGFloat *blue);
|
||||
|
||||
// Hue is a value between 0 and 360, saturation and lightness between 0 and 100. Stores the RGB in values between 0 and 1.
|
||||
void hsluvToRgb(CGFloat hue, CGFloat saturation, CGFloat lightness, CGFloat *red, CGFloat *green, CGFloat *blue);
|
||||
|
||||
// Red, green and blue values between 0 and 1, stores the hsluv components with hue between 0 and 360, saturation and lightness between 0 and 100.
|
||||
void rgbToHsluv(CGFloat red, CGFloat green, CGFloat blue, CGFloat *hue, CGFloat *saturation, CGFloat *lightness);
|
||||
|
||||
// Hue is a value between 0 and 360, saturation and lightness between 0 and 100. Stores the RGB in values between 0 and 1.
|
||||
void hpluvToRgb(CGFloat hue, CGFloat saturation, CGFloat lightness, CGFloat *red, CGFloat *green, CGFloat *blue);
|
||||
|
||||
// Red, green and blue values between 0 and 1, stores the hpluv components with hue between 0 and 360, saturation and lightness between 0 and 100.
|
||||
void rgbToHpluv(CGFloat red, CGFloat green, CGFloat blue, CGFloat *hue, CGFloat *saturation, CGFloat *lightness);
|
||||
|
||||
#endif
|
205
Sources/JolkEngine/Application.swift
Normal file
@ -0,0 +1,205 @@
|
||||
import Foundation
|
||||
import SDL
|
||||
|
||||
|
||||
public protocol ApplicationImplementation
|
||||
{
|
||||
func create(render: inout Renderer)
|
||||
func quit()
|
||||
func loadContent(content: inout ContentManager) throws
|
||||
func resize(width: Int, height: Int)
|
||||
func update(deltaTime: Float)
|
||||
func draw(render: inout Renderer, deltaTime: Float)
|
||||
}
|
||||
|
||||
extension ApplicationImplementation
|
||||
{
|
||||
public func quit() {}
|
||||
}
|
||||
|
||||
public struct ApplicationConfiguration
|
||||
{
|
||||
let resizable: Bool
|
||||
let vSync: VSyncMode
|
||||
let windowWidth: Int32
|
||||
let windowHeight: Int32
|
||||
let bundle: Bundle
|
||||
|
||||
public init(resizable: Bool, vSync: VSyncMode, windowWidth: Int32, windowHeight: Int32, bundle: Bundle)
|
||||
{
|
||||
self.resizable = resizable
|
||||
self.vSync = vSync
|
||||
self.windowWidth = windowWidth
|
||||
self.windowHeight = windowHeight
|
||||
self.bundle = bundle
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//FIXME: Wrap these
|
||||
public var sdlPad: OpaquePointer? = nil
|
||||
var joyId: Int32 = -1
|
||||
|
||||
public class Application
|
||||
{
|
||||
private let implementation: ApplicationImplementation
|
||||
private let configuration: ApplicationConfiguration
|
||||
|
||||
private var window: OpaquePointer?
|
||||
private var render: Renderer
|
||||
private var content: ContentManager
|
||||
|
||||
typealias ResizeEvent = (width: Int32, height: Int32)
|
||||
private var resize: ResizeEvent? = nil
|
||||
|
||||
func openController(index: Int32) -> (pad: OpaquePointer, joy: Int32)?
|
||||
{
|
||||
guard let sdlPad = SDL_GameControllerOpen(index) else { return nil }
|
||||
let joyId = SDL_JoystickGetDeviceInstanceID(index)
|
||||
print("Using gamepad #\(joyId), \"\(String(cString: SDL_GameControllerName(sdlPad)))\"")
|
||||
return (pad: sdlPad, joy: joyId)
|
||||
}
|
||||
|
||||
public init(application: ApplicationImplementation, config: ApplicationConfiguration)
|
||||
{
|
||||
self.implementation = application
|
||||
self.configuration = config
|
||||
self.render = OpenGL(version: .init(major: 1, minor: 5))
|
||||
self.content = ContentManager(&self.render, bundle: configuration.bundle)
|
||||
}
|
||||
|
||||
public func run()
|
||||
{
|
||||
guard SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) == 0
|
||||
else { fatalError("SDL_Init: \(String(cString: SDL_GetError()))") }
|
||||
|
||||
let winPos = Int32(SDL_WINDOWPOS_UNDEFINED_MASK)
|
||||
let winWidth: Int32 = configuration.windowWidth, winHeight: Int32 = configuration.windowHeight
|
||||
var winFlags = SDL_WINDOW_RESIZABLE.rawValue | SDL_WINDOW_ALLOW_HIGHDPI.rawValue
|
||||
if render is OpenGL { winFlags |= SDL_WINDOW_OPENGL.rawValue }
|
||||
window = SDL_CreateWindow("Something", winPos, winPos, winWidth, winHeight, winFlags)
|
||||
guard window != nil else { fatalError("SDL_CreateWindow: \(String(cString: SDL_GetError()))") }
|
||||
|
||||
do
|
||||
{
|
||||
try render.create(sdlWindow: window!)
|
||||
try render.setVsync(mode: configuration.vSync)
|
||||
}
|
||||
catch RendererError.sdlError(let message)
|
||||
{
|
||||
fatalError("idk error \(message)")
|
||||
}
|
||||
catch { fatalError("piss") }
|
||||
|
||||
for i in 0...SDL_NumJoysticks()
|
||||
{
|
||||
if SDL_IsGameController(i) != SDL_TRUE { continue }
|
||||
if let open = openController(index: i)
|
||||
{
|
||||
sdlPad = open.pad
|
||||
joyId = open.joy
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
implementation.create(render: &render)
|
||||
|
||||
content.setLoader(extension: "obj", loader: ObjLoader())
|
||||
content.setLoader(extension: "g3db", loader: G3DbLoader())
|
||||
content.setLoader(extensions: ["png", "jpg"], loader: NSImageLoader())
|
||||
try implementation.loadContent(content: &content)
|
||||
}
|
||||
catch
|
||||
{
|
||||
fatalError("loadContent()")
|
||||
}
|
||||
|
||||
let timeMul = 1 / Double(SDL_GetPerformanceFrequency());
|
||||
var prevTime = SDL_GetPerformanceCounter();
|
||||
|
||||
resize = displaySize
|
||||
|
||||
mainLoop: while true
|
||||
{
|
||||
Input.instance.newTick()
|
||||
|
||||
var event = SDL_Event()
|
||||
while SDL_PollEvent(&event) > 0
|
||||
{
|
||||
switch SDL_EventType(event.type)
|
||||
{
|
||||
case SDL_QUIT: break mainLoop
|
||||
case SDL_KEYDOWN:
|
||||
if event.key.repeat == 0
|
||||
{
|
||||
Input.instance.pressEvent(scan: event.key.keysym.scancode)
|
||||
}
|
||||
case SDL_KEYUP:
|
||||
Input.instance.releaseEvent(scan: event.key.keysym.scancode)
|
||||
case SDL_WINDOWEVENT:
|
||||
switch SDL_WindowEventID(UInt32(event.window.event))
|
||||
{
|
||||
case SDL_WINDOWEVENT_RESIZED:
|
||||
resize = displaySize
|
||||
default: break
|
||||
}
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
if sdlPad == nil && SDL_IsGameController(event.cdevice.which) == SDL_TRUE
|
||||
{
|
||||
if let open = openController(index: event.cdevice.which)
|
||||
{
|
||||
sdlPad = open.pad
|
||||
joyId = open.joy
|
||||
}
|
||||
}
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
if event.cdevice.which == joyId
|
||||
{
|
||||
SDL_GameControllerClose(sdlPad)
|
||||
sdlPad = nil
|
||||
joyId = -1
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
let time = SDL_GetPerformanceCounter();
|
||||
let deltaTime = Float(Double(time - prevTime) * timeMul)
|
||||
prevTime = time
|
||||
|
||||
implementation.update(deltaTime: min(deltaTime, 1.0 / 15.0))
|
||||
repaint(deltaTime)
|
||||
}
|
||||
|
||||
implementation.quit()
|
||||
|
||||
content.releaseAll()
|
||||
render.delete()
|
||||
SDL_DestroyWindow(window)
|
||||
SDL_Quit()
|
||||
}
|
||||
|
||||
private func repaint(_ deltaTime: Float)
|
||||
{
|
||||
if resize != nil
|
||||
{
|
||||
render.resize(width: resize!.width, height: resize!.height)
|
||||
implementation.resize(width: Int(resize!.width), height: Int(resize!.height))
|
||||
resize = nil
|
||||
}
|
||||
|
||||
render.newFrame()
|
||||
implementation.draw(render: &render, deltaTime: deltaTime)
|
||||
|
||||
SDL_GL_SwapWindow(window)
|
||||
}
|
||||
|
||||
private var displaySize: ResizeEvent
|
||||
{
|
||||
var width: Int32 = 0, height: Int32 = 0
|
||||
SDL_GL_GetDrawableSize(window, &width, &height)
|
||||
return (width, height)
|
||||
}
|
||||
}
|
95
Sources/JolkEngine/Colour.swift
Normal file
@ -0,0 +1,95 @@
|
||||
import SDL2
|
||||
import HSLuv
|
||||
|
||||
|
||||
public struct Colour: Equatable { let r, g, b, a: Float }
|
||||
|
||||
public extension Colour
|
||||
{
|
||||
static var zero: Self { .init(r: 0, g: 0, b: 0, a: 0) }
|
||||
static var black: Self { .init(r: 0, g: 0, b: 0, a: 1) }
|
||||
static var white: Self { .init(r: 1, g: 1, b: 1, a: 1) }
|
||||
|
||||
init(r: Float, g: Float, b: Float)
|
||||
{
|
||||
self.init(r: r, g: g, b: b, a: 1.0)
|
||||
}
|
||||
|
||||
init(fromSdl: SDL_Color)
|
||||
{
|
||||
self.init(
|
||||
r: Float(fromSdl.r) / 0xFF,
|
||||
g: Float(fromSdl.g) / 0xFF,
|
||||
b: Float(fromSdl.b) / 0xFF,
|
||||
a: Float(fromSdl.a) / 0xFF)
|
||||
}
|
||||
|
||||
init(fromRgb24: UInt32)
|
||||
{
|
||||
self.init(
|
||||
r: Float((fromRgb24 >> 16) & 0xFF) / 0xFF,
|
||||
g: Float((fromRgb24 >> 8) & 0xFF) / 0xFF,
|
||||
b: Float((fromRgb24 >> 0) & 0xFF) / 0xFF,
|
||||
a: 1)
|
||||
}
|
||||
|
||||
init(fromRgba32: UInt32)
|
||||
{
|
||||
self.init(
|
||||
r: Float((fromRgba32 >> 24) & 0xFF) / 0xFF,
|
||||
g: Float((fromRgba32 >> 16) & 0xFF) / 0xFF,
|
||||
b: Float((fromRgba32 >> 8) & 0xFF) / 0xFF,
|
||||
a: Float((fromRgba32 >> 0) & 0xFF) / 0xFF)
|
||||
}
|
||||
|
||||
func mix(with rhs: Colour, _ amount: Float) -> Colour
|
||||
{
|
||||
let x = amount.saturate
|
||||
return Colour(
|
||||
r: Float.lerp(r, rhs.r, x),
|
||||
g: Float.lerp(g, rhs.g, x),
|
||||
b: Float.lerp(b, rhs.b, x),
|
||||
a: Float.lerp(a, rhs.a, x))
|
||||
}
|
||||
|
||||
func lighten(by: Double) -> Colour
|
||||
{
|
||||
var hue = CGFloat(), sat = CGFloat(), light = CGFloat()
|
||||
rgbToHsluv(CGFloat(r), CGFloat(g), CGFloat(b), &hue, &sat, &light)
|
||||
let gamma: CGFloat = 3.0
|
||||
light = pow((pow(light / 100, 1 / gamma) + CGFloat(by) * 0.8).saturate, gamma) * 100
|
||||
var newR = CGFloat(), newG = CGFloat(), newB = CGFloat()
|
||||
hsluvToRgb(hue, sat, light, &newR, &newG, &newB)
|
||||
return Colour(r: Float(newR), g: Float(newG), b: Float(newB), a: a)
|
||||
}
|
||||
|
||||
var linear: Colour { get
|
||||
{
|
||||
Colour(
|
||||
r: linearFromSrgb(r),
|
||||
g: linearFromSrgb(g),
|
||||
b: linearFromSrgb(b),
|
||||
a: a)
|
||||
} }
|
||||
|
||||
func setAlpha(_ newAlpha: Float) -> Colour
|
||||
{
|
||||
return Colour(r: r, g: g, b: b, a: newAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@inline(__always) fileprivate func srgbFromLinear(_ x: Float) -> Float
|
||||
{
|
||||
return (x < 0.0031308)
|
||||
? x * 12.92
|
||||
: 1.055 * pow(x, 1.0 / 2.4) - 0.055
|
||||
}
|
||||
|
||||
@inline(__always) fileprivate func linearFromSrgb(_ x: Float) -> Float
|
||||
{
|
||||
return (x < 0.04045)
|
||||
? x / 12.92
|
||||
: pow((x + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
165
Sources/JolkEngine/Content/ContentManager.swift
Normal file
@ -0,0 +1,165 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public protocol ContentLoaderParametersProtocol
|
||||
{
|
||||
associatedtype T: Resource
|
||||
}
|
||||
|
||||
public struct ContentManager
|
||||
{
|
||||
private var loaders = Dictionary<String, any LoaderProtocol>()
|
||||
private var resources = [any RendererResource]()
|
||||
private let renderer: Renderer
|
||||
private let bundle: Bundle
|
||||
}
|
||||
|
||||
extension ContentManager
|
||||
{
|
||||
internal init(_ renderer: inout Renderer, bundle: Bundle)
|
||||
{
|
||||
self.renderer = renderer
|
||||
self.bundle = bundle
|
||||
}
|
||||
|
||||
public mutating func setLoader<T: Resource, Loader: LoaderProtocol>(extensions exts: [String], loader: Loader) where Loader.T == T
|
||||
{
|
||||
for i in exts
|
||||
{
|
||||
setLoader(extension: i, loader: loader)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func setLoader<T: Resource, Loader: LoaderProtocol>(extension ext: String, loader: Loader) where Loader.T == T
|
||||
{
|
||||
loaders[ext] = loader
|
||||
}
|
||||
|
||||
public mutating func create(mesh: Mesh) throws -> RenderMesh
|
||||
{
|
||||
let rendMesh = try renderer.createMesh(mesh: mesh)
|
||||
resources.append(rendMesh)
|
||||
return rendMesh
|
||||
}
|
||||
|
||||
public mutating func create(texture: Image, params: Texture2DParameters = .init()) throws -> Texture2D
|
||||
{
|
||||
try texture.pixels.withUnsafeBytes
|
||||
{
|
||||
raw in
|
||||
let data = raw.baseAddress!
|
||||
let rendTex = try renderer.createTexture(
|
||||
data: data, width: texture.width, height: texture.height,
|
||||
filter: params.magFilter, mipMode: params.mipMode)
|
||||
resources.append(rendTex)
|
||||
return Texture2D(id: rendTex, width: texture.width, height: texture.height)
|
||||
}
|
||||
}
|
||||
|
||||
public func getResource(_ path: String) throws -> URL
|
||||
{
|
||||
guard let extIndex = path.lastIndex(of: ".")
|
||||
else { throw ContentError.badPath }
|
||||
let name = String(path[..<extIndex]), ext = String(path[extIndex...])
|
||||
guard let resourceUrl: URL = bundle.url(
|
||||
forResource: name,
|
||||
withExtension: ext)
|
||||
else { throw ContentError.resourceNotFound }
|
||||
return resourceUrl
|
||||
}
|
||||
|
||||
public mutating func load<T: Resource>(_ path: String) throws -> T
|
||||
{
|
||||
let resourceUrl = try getResource(path)
|
||||
|
||||
let loader = loaders[resourceUrl.pathExtension]
|
||||
guard let resource = loader?.load(url: resourceUrl)
|
||||
else { throw ContentError.loadFailure }
|
||||
|
||||
if T.self == Mesh.self
|
||||
{
|
||||
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
|
||||
return mesh as! T
|
||||
}
|
||||
if T.self == Image.self
|
||||
{
|
||||
guard let image = resource as? Image else { throw ContentError.loadFailure }
|
||||
return image as! T
|
||||
}
|
||||
if T.self == Texture2D.self
|
||||
{
|
||||
guard let image = resource as? Image else { throw ContentError.loadFailure }
|
||||
let texture2D = try self.create(texture: image)
|
||||
return texture2D as! T
|
||||
}
|
||||
throw ContentError.loadFailure
|
||||
}
|
||||
|
||||
public mutating func load<T: Resource, Params: ContentLoaderParametersProtocol>(_ path: String, params: Params) throws
|
||||
-> T where Params.T == T
|
||||
{
|
||||
let resourceUrl = try getResource(path)
|
||||
|
||||
let loader = loaders[resourceUrl.pathExtension]
|
||||
guard let resource = loader?.load(url: resourceUrl)
|
||||
else { throw ContentError.loadFailure }
|
||||
|
||||
if T.self == Mesh.self
|
||||
{
|
||||
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
|
||||
return mesh as! T
|
||||
}
|
||||
if T.self == Image.self
|
||||
{
|
||||
guard let image = resource as? Image else { throw ContentError.loadFailure }
|
||||
return image as! T
|
||||
}
|
||||
if T.self == Texture2D.self
|
||||
{
|
||||
guard let image = resource as? Image else { throw ContentError.loadFailure }
|
||||
let texture2D = try self.create(texture: image, params: params as! Texture2DParameters)
|
||||
return texture2D as! T
|
||||
}
|
||||
throw ContentError.loadFailure
|
||||
}
|
||||
|
||||
public mutating func load<T: RendererResource>(_ path: String) throws -> T
|
||||
{
|
||||
let resourceUrl = try getResource(path)
|
||||
|
||||
let loader = loaders[resourceUrl.pathExtension]
|
||||
guard let resource = loader?.load(url: resourceUrl)
|
||||
else { throw ContentError.loadFailure }
|
||||
|
||||
if T.self == RenderMesh.self
|
||||
{
|
||||
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
|
||||
let renderResource = try self.create(mesh: mesh)
|
||||
return renderResource as! T
|
||||
}
|
||||
throw ContentError.loadFailure
|
||||
}
|
||||
|
||||
internal mutating func releaseAll()
|
||||
{
|
||||
for resource in resources.reversed()
|
||||
{
|
||||
if resource is RenderMesh
|
||||
{
|
||||
renderer.deleteMesh(resource as! RenderMesh)
|
||||
}
|
||||
else if resource is RenderTexture2D
|
||||
{
|
||||
renderer.deleteTexture(resource as! RenderTexture2D)
|
||||
}
|
||||
}
|
||||
resources.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
public enum ContentError: Error
|
||||
{
|
||||
case badPath //else { fatalError("Malformed resource path \"\(path)\"") }
|
||||
case resourceNotFound //else { fatalError("Resource \"\(path)\" doesn't exist") }
|
||||
case loadFailure //else { fatalError("Failed to load resource \"\(path)\"") }
|
||||
}
|
9
Sources/JolkEngine/Content/Image.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct Image: Resource
|
||||
{
|
||||
let pixels: Data
|
||||
let width: Int
|
||||
let height: Int
|
||||
}
|
9
Sources/JolkEngine/Content/LoaderProtocol.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public protocol LoaderProtocol
|
||||
{
|
||||
associatedtype T: Resource
|
||||
|
||||
func load(url: URL) -> T?
|
||||
}
|
54
Sources/JolkEngine/Content/Mesh.swift
Normal file
@ -0,0 +1,54 @@
|
||||
import OrderedCollections
|
||||
|
||||
|
||||
public struct Model: Resource
|
||||
{
|
||||
public let meshes: [Mesh]
|
||||
}
|
||||
|
||||
public struct Mesh: Resource
|
||||
{
|
||||
public typealias Index = UInt16
|
||||
|
||||
public struct Vertex: Equatable
|
||||
{
|
||||
public let position: Vec3f
|
||||
public let normal: Vec3f
|
||||
public let texCoord: Vec2f
|
||||
|
||||
public init(position: Vec3f, normal: Vec3f, texCoord: Vec2f)
|
||||
{
|
||||
self.position = position
|
||||
self.normal = normal
|
||||
self.texCoord = texCoord
|
||||
}
|
||||
}
|
||||
|
||||
public struct SubMesh
|
||||
{
|
||||
public let start, length: Int
|
||||
|
||||
public init(start: Int, length: Int)
|
||||
{
|
||||
self.start = start
|
||||
self.length = length
|
||||
}
|
||||
}
|
||||
|
||||
public let vertices: [Vertex]
|
||||
public let indices: [Index]
|
||||
public let subMeshes: OrderedDictionary<String, SubMesh>
|
||||
}
|
||||
|
||||
public extension Mesh
|
||||
{
|
||||
static let empty = Self(vertices: .init(), indices: .init(), subMeshes: .init())
|
||||
|
||||
init(vertices: [Vertex], indices: [Index])
|
||||
{
|
||||
self.init(
|
||||
vertices: vertices,
|
||||
indices: indices,
|
||||
subMeshes: .init())
|
||||
}
|
||||
}
|
120
Sources/JolkEngine/Content/ObjModel.swift
Normal file
@ -0,0 +1,120 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct ObjModel
|
||||
{
|
||||
public var positions = [Vec3f]()
|
||||
public var normals = [Vec3f]()
|
||||
public var texCoords = [Vec2f]()
|
||||
public var objects = Dictionary<String, Object>()
|
||||
|
||||
public struct Object { public var faces = [Face]() }
|
||||
|
||||
public struct Index { public let p: Int, n: Int, t: Int }
|
||||
public enum Face
|
||||
{
|
||||
case line(p1: Int, p2: Int)
|
||||
case triangle(v1: Index, v2: Index, v3: Index)
|
||||
case quad(v1: Index, v2: Index, v3: Index, v4: Index)
|
||||
case ngon(_ v: [Index])
|
||||
}
|
||||
}
|
||||
|
||||
public struct ObjMaterial
|
||||
{
|
||||
var model: IlluminationModel = .lambert
|
||||
var ambient: Colour = .black
|
||||
var diffuse: Colour = .white
|
||||
var specular: Colour = .zero
|
||||
var specularExp: Float = 30
|
||||
var refraction: Float = 1.0
|
||||
var alpha: Float = 1.0
|
||||
|
||||
var ambientMap: TextureMap? = nil
|
||||
var diffuseMap: TextureMap? = nil
|
||||
var specularMap: TextureMap? = nil
|
||||
var specularExpMap: TextureMap? = nil
|
||||
var dissolveMap: TextureMap? = nil
|
||||
var bumpMap: TextureMap? = nil
|
||||
var displacementMap: TextureMap? = nil
|
||||
var decalMap: TextureMap? = nil
|
||||
|
||||
// PBR extensions
|
||||
var roughness: Float = 0.5
|
||||
var metallic: Float = 0.0
|
||||
var sheen: Float = 0.0
|
||||
var clearcoatThickness: Float = 0.0
|
||||
var clearcoatRoughness: Float = 0.03 // dunno what the default should be so here's the blender one
|
||||
var emmision: Vec3f = .zero
|
||||
var anisotropy: Float = 0.0
|
||||
var anisotropyRotation: Float = 0.0
|
||||
|
||||
var roughnessMap: TextureMap? = nil
|
||||
var metallicMap: TextureMap? = nil
|
||||
var sheenMap: TextureMap? = nil
|
||||
var emmisionMap: TextureMap? = nil
|
||||
var normalMap: TextureMap? = nil
|
||||
|
||||
// Ft Fresnel reflectance
|
||||
// Ft Fresnel transmittance
|
||||
// Ns Specular exponent/shininess
|
||||
// Ia Ambient light
|
||||
// I Light intensity
|
||||
// Ir Intensity from reflected direction (reflection map and/or raytracing)
|
||||
// It Intensity from transmitted direction
|
||||
// Ka Ambient reflectance
|
||||
// Kd Diffuse reflectance
|
||||
// Ks Specular reflectance
|
||||
// Tf Transmission filter
|
||||
// H Unit vector bisector between L and V
|
||||
// L Unit light vector
|
||||
// N Unit surface normal
|
||||
// V Unit view vector
|
||||
//
|
||||
// j = piss
|
||||
// Fr = Fresnel
|
||||
// lambert = KaIa + Kd(N * Lj)Ij
|
||||
// blinnPhong = lambert + Ks((H * Hj) ^ Ns)Ij
|
||||
//
|
||||
// Notes: Clara.io only usees 0 and 1, maya always exports 4, blender can export 1, 3, 6
|
||||
public enum IlluminationModel
|
||||
{
|
||||
case colour // Kd only
|
||||
case lambert
|
||||
case blinnPhong
|
||||
case reflectionRaytrace // + Ir = (intensity of reflection map) + (ray trace)
|
||||
case transparentGlassReflectionRaytrace // + Ir = (glass) + (intensity of reflection map) + (raytrace)
|
||||
case reflectionRaytraceFresnel // Fr(Lj * Hj, Ks, Ns)Ij + Fr(N * V, Ks, Ns)Ir
|
||||
case transparentRefractionReflectionRaytrace // + Ir + (1 - Ks)TfIt
|
||||
case transparentRefractionReflectionRaytraceFresnel // Fr(Lj * Hj, Ks, Ns)Ij + Fr(N * V, Ks, Ns)Ir
|
||||
case reflection // + Ir = (intensity of reflection map)
|
||||
case transparentGlassReflection // + Ir = (glass) + (intensity of reflection map) + (raytrace)
|
||||
case shadowOnly
|
||||
}
|
||||
|
||||
public struct TextureMap
|
||||
{
|
||||
var path: String = .init()
|
||||
var flags: Flags = [ .blendHoriz, .blendVert ]
|
||||
var blendMul: Float = 1.0
|
||||
var boost: Float = 0.0 // non-negative
|
||||
var imfChan: ImfChan = .l // decal ? .m : .l
|
||||
var mmBaseGain: (Float, Float) = (0.0, 1.0)
|
||||
var offset: Vec3f = .zero
|
||||
var scale: Vec3f = .one
|
||||
var turbulence: Vec3f = .zero
|
||||
var textureResolution: Int = 0
|
||||
|
||||
struct Flags: OptionSet
|
||||
{
|
||||
let rawValue: UInt8
|
||||
|
||||
static let blendHoriz: Self = Self(rawValue: 1 << 0)
|
||||
static let blendVert: Self = Self(rawValue: 1 << 1)
|
||||
static let colourCorrection: Self = Self(rawValue: 1 << 2)
|
||||
static let clamp: Self = Self(rawValue: 1 << 3)
|
||||
}
|
||||
|
||||
enum ImfChan { case r, g, b, m, l, z }
|
||||
}
|
||||
}
|
4
Sources/JolkEngine/Content/Resource.swift
Normal file
@ -0,0 +1,4 @@
|
||||
public protocol Resource
|
||||
{
|
||||
|
||||
}
|
155
Sources/JolkEngine/Input.swift
Normal file
@ -0,0 +1,155 @@
|
||||
import SDL2
|
||||
|
||||
|
||||
public class Input
|
||||
{
|
||||
private static let _instance = Input()
|
||||
|
||||
public static var instance: Input { _instance }
|
||||
|
||||
//private let _UP: UInt8 = 0
|
||||
//private let _PRESS: UInt8 = 1
|
||||
//private let _DOWN: UInt8 = 2
|
||||
//private let _RELEASE: UInt8 = 3
|
||||
|
||||
private static let _UP: UInt8 = 0b000
|
||||
private static let _DOWN: UInt8 = 0b010
|
||||
private static let _IMPULSE: UInt8 = 0b001
|
||||
private static let _REPEAT: UInt8 = 0b100
|
||||
private static let _PRESS: UInt8 = _DOWN | _IMPULSE
|
||||
private static let _RELEASE: UInt8 = _UP | _IMPULSE
|
||||
|
||||
private var _keys = [UInt8](repeating: _UP, count: Int(SDL_NUM_SCANCODES.rawValue))
|
||||
|
||||
private init() {}
|
||||
|
||||
internal func newTick()
|
||||
{
|
||||
//_keys = _keys.map({ i in (i & 0x1) == 0x1 ? ((i &+ 1) & 0x3) : i })
|
||||
_keys = _keys.map({ $0 & ~Input._IMPULSE })
|
||||
//for i in 0..<Int(SDL_NUM_SCANCODES.rawValue)
|
||||
//{
|
||||
// _keys[i] &= ~0x1
|
||||
//}
|
||||
}
|
||||
|
||||
internal func pressEvent(scan: SDL_Scancode)
|
||||
{
|
||||
_keys[Int(scan.rawValue)] = Input._PRESS
|
||||
}
|
||||
|
||||
internal func releaseEvent(scan: SDL_Scancode)
|
||||
{
|
||||
_keys[Int(scan.rawValue)] = Input._RELEASE
|
||||
}
|
||||
|
||||
public func keyDown(_ key: Key) -> Bool
|
||||
{
|
||||
//SDL_GetKeyboardState(nil).withMemoryRebound(to: UInt8.self, capacity: Int(SDL_NUM_SCANCODES.rawValue))
|
||||
//{ state in
|
||||
// state[Int(key.sdlScancode.rawValue)] != 0
|
||||
//}
|
||||
_keys[Int(key.sdlScancode.rawValue)] & Input._DOWN == Input._DOWN
|
||||
}
|
||||
|
||||
public func keyPressed(_ key: Key) -> Bool
|
||||
{
|
||||
_keys[Int(key.sdlScancode.rawValue)] == Input._PRESS
|
||||
}
|
||||
|
||||
public func keyReleased(_ key: Key) -> Bool
|
||||
{
|
||||
_keys[Int(key.sdlKeycode.rawValue)] == Input._RELEASE
|
||||
}
|
||||
}
|
||||
|
||||
public enum Key
|
||||
{
|
||||
case 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
|
||||
case right, left, down, up
|
||||
}
|
||||
|
||||
extension Key
|
||||
{
|
||||
internal var sdlKeycode: SDL_KeyCode
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .a: SDLK_a
|
||||
case .b: SDLK_a
|
||||
case .c: SDLK_a
|
||||
case .d: SDLK_a
|
||||
case .e: SDLK_a
|
||||
case .f: SDLK_a
|
||||
case .g: SDLK_a
|
||||
case .h: SDLK_a
|
||||
case .i: SDLK_a
|
||||
case .j: SDLK_a
|
||||
case .k: SDLK_a
|
||||
case .l: SDLK_a
|
||||
case .m: SDLK_a
|
||||
case .n: SDLK_a
|
||||
case .o: SDLK_a
|
||||
case .p: SDLK_a
|
||||
case .q: SDLK_a
|
||||
case .r: SDLK_a
|
||||
case .s: SDLK_a
|
||||
case .t: SDLK_a
|
||||
case .u: SDLK_a
|
||||
case .v: SDLK_a
|
||||
case .w: SDLK_a
|
||||
case .x: SDLK_a
|
||||
case .y: SDLK_a
|
||||
case .z: SDLK_a
|
||||
case .left: SDLK_LEFT
|
||||
case .right: SDLK_RIGHT
|
||||
case .up: SDLK_UP
|
||||
case .down: SDLK_DOWN
|
||||
}
|
||||
}
|
||||
|
||||
internal var sdlScancode: SDL_Scancode
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .a: SDL_SCANCODE_A
|
||||
case .b: SDL_SCANCODE_B
|
||||
case .c: SDL_SCANCODE_C
|
||||
case .d: SDL_SCANCODE_D
|
||||
case .e: SDL_SCANCODE_E
|
||||
case .f: SDL_SCANCODE_F
|
||||
case .g: SDL_SCANCODE_G
|
||||
case .h: SDL_SCANCODE_H
|
||||
case .i: SDL_SCANCODE_I
|
||||
case .j: SDL_SCANCODE_J
|
||||
case .k: SDL_SCANCODE_K
|
||||
case .l: SDL_SCANCODE_L
|
||||
case .m: SDL_SCANCODE_M
|
||||
case .n: SDL_SCANCODE_N
|
||||
case .o: SDL_SCANCODE_O
|
||||
case .p: SDL_SCANCODE_P
|
||||
case .q: SDL_SCANCODE_Q
|
||||
case .r: SDL_SCANCODE_R
|
||||
case .s: SDL_SCANCODE_S
|
||||
case .t: SDL_SCANCODE_T
|
||||
case .u: SDL_SCANCODE_U
|
||||
case .v: SDL_SCANCODE_V
|
||||
case .w: SDL_SCANCODE_W
|
||||
case .x: SDL_SCANCODE_X
|
||||
case .y: SDL_SCANCODE_Y
|
||||
case .z: SDL_SCANCODE_Z
|
||||
case .left: SDL_SCANCODE_LEFT
|
||||
case .right: SDL_SCANCODE_RIGHT
|
||||
case .up: SDL_SCANCODE_UP
|
||||
case .down: SDL_SCANCODE_DOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SDL_KeyCode
|
||||
{
|
||||
internal init(scancode: SDL_Scancode)
|
||||
{
|
||||
self.init(scancode.rawValue | UInt32(SDLK_SCANCODE_MASK))
|
||||
}
|
||||
}
|
249
Sources/JolkEngine/Loaders/G3DbLoader.swift
Normal file
@ -0,0 +1,249 @@
|
||||
import Foundation
|
||||
import OrderedCollections
|
||||
|
||||
|
||||
class G3DbLoader: LoaderProtocol
|
||||
{
|
||||
typealias T = Mesh
|
||||
|
||||
func load(url: URL) -> T?
|
||||
{
|
||||
guard var reader = try? G3DbReader(url)
|
||||
else { return Optional.none }
|
||||
return try? reader.read()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct G3DbReader
|
||||
{
|
||||
private let version: [Int16] = [0, 1]
|
||||
private var reader: UBJsonReader
|
||||
|
||||
init(_ url: URL) throws
|
||||
{
|
||||
guard let file = try? FileHandle(forReadingFrom: url)
|
||||
else { throw G3DbReaderError.fileNotFound }
|
||||
self.reader = UBJsonReader(file: file)
|
||||
}
|
||||
|
||||
mutating func read() throws -> Mesh
|
||||
{
|
||||
let model = try readModel()
|
||||
|
||||
let mesh = model.meshes[0]
|
||||
|
||||
var vertices = [Mesh.Vertex]()
|
||||
var indices = [UInt16]()
|
||||
var subMeshes = OrderedDictionary<String, Mesh.SubMesh>()
|
||||
|
||||
let attributeSize =
|
||||
{ (attrib: G3DAttribute) in
|
||||
switch attrib
|
||||
{
|
||||
case .position, .normal, .colour, .tangent, .bitangent:
|
||||
return 3
|
||||
case .texCoord(_), .blendWeight(_):
|
||||
return 2
|
||||
case .colourPacked:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
var stride = 0
|
||||
for a in mesh.attributes
|
||||
{
|
||||
stride += attributeSize(a)
|
||||
}
|
||||
let numVerts = mesh.vertices.count / stride
|
||||
vertices.reserveCapacity(numVerts)
|
||||
for i in 0..<numVerts
|
||||
{
|
||||
var srcIdx = i * stride
|
||||
var position: Vec3f = .zero, normal: Vec3f = .zero
|
||||
var texCoord: Vec2f = .zero
|
||||
for a in mesh.attributes
|
||||
{
|
||||
switch a
|
||||
{
|
||||
case .position:
|
||||
position = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
mesh.vertices[srcIdx + 2],
|
||||
-mesh.vertices[srcIdx + 1])
|
||||
case .normal:
|
||||
normal = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
mesh.vertices[srcIdx + 2],
|
||||
-mesh.vertices[srcIdx + 1])
|
||||
case .texCoord(id: 0):
|
||||
texCoord = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
1.0 - mesh.vertices[srcIdx + 1])
|
||||
default: break
|
||||
}
|
||||
srcIdx += attributeSize(a)
|
||||
}
|
||||
vertices.append(.init(position: position, normal: normal, texCoord: texCoord))
|
||||
}
|
||||
for part in mesh.parts
|
||||
{
|
||||
subMeshes[part.key] = .init(start: indices.count, length: part.value.indices.count)
|
||||
indices += part.value.indices.map { UInt16($0) }
|
||||
}
|
||||
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
|
||||
}
|
||||
|
||||
mutating func readModel() throws -> G3DModel
|
||||
{
|
||||
let root = try reader.read()
|
||||
let version = try root.getArray(key: "version")
|
||||
guard try version.count == 2 && (try version.map({ try $0.int16 }) == self.version)
|
||||
else { throw G3DbReaderError.versionMismatch }
|
||||
|
||||
var model = G3DModel()
|
||||
model.id = try root.getString(key: "id", default: "")
|
||||
try readMeshes(root, &model)
|
||||
try readMaterials(root, &model)
|
||||
return model
|
||||
}
|
||||
|
||||
mutating func readMeshes(_ root: UBJsonToken, _ model: inout G3DModel) throws
|
||||
{
|
||||
let meshes = try root.getArray(key: "meshes")
|
||||
for obj in meshes
|
||||
{
|
||||
var mesh = G3DModelMesh()
|
||||
|
||||
mesh.id = try obj.getString(key: "id", default: "")
|
||||
for attrib in try obj.getArray(key: "attributes")
|
||||
{
|
||||
mesh.attributes.append(try .resolve(try attrib.string))
|
||||
}
|
||||
mesh.vertices = try obj.getFloatArray(key: "vertices")
|
||||
for partObj in try obj.getArray(key: "parts")
|
||||
{
|
||||
let id = try partObj.getString(key: "id")
|
||||
if mesh.parts.keys.contains(id) { throw G3DbReaderError.duplicateIDs }
|
||||
|
||||
var part = G3dModelMeshPart()
|
||||
part.mode = try .resolve(try partObj.getString(key: "type"))
|
||||
part.indices = try partObj.getInt16Array(key: "indices")
|
||||
|
||||
mesh.parts[id] = part
|
||||
}
|
||||
model.meshes.append(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func readMaterials(_ root: UBJsonToken, _ model: inout G3DModel) throws
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct G3DModel
|
||||
{
|
||||
var id: String = .init()
|
||||
var meshes: [G3DModelMesh] = .init()
|
||||
}
|
||||
|
||||
fileprivate struct G3DModelMesh
|
||||
{
|
||||
var id: String = .init()
|
||||
var attributes: [G3DAttribute] = .init()
|
||||
var vertices: [Float] = .init()
|
||||
var parts: Dictionary<String, G3dModelMeshPart> = .init()
|
||||
}
|
||||
|
||||
fileprivate struct G3dModelMeshPart
|
||||
{
|
||||
var mode: G3DPrimativeType = .invalid
|
||||
var indices: [Int16] = .init()
|
||||
}
|
||||
|
||||
fileprivate enum G3DAttribute: Equatable
|
||||
{
|
||||
case position
|
||||
case normal
|
||||
case tangent
|
||||
case bitangent
|
||||
case colour
|
||||
case colourPacked
|
||||
case texCoord(id: UInt8)
|
||||
case blendWeight(id: UInt8)
|
||||
|
||||
static let order = [ .position, .colour, .colourPacked, .normal,
|
||||
texCoord(id: 0), .blendWeight(id: 0), .tangent, .bitangent ]
|
||||
}
|
||||
|
||||
extension G3DAttribute
|
||||
{
|
||||
static func resolve(_ attrib: String) throws -> Self
|
||||
{
|
||||
let getAttributeId =
|
||||
{ (attrib: String, offset: Int) throws -> UInt8 in
|
||||
let idIdx = attrib.index(attrib.startIndex, offsetBy: offset)
|
||||
let idStr = attrib.suffix(from: idIdx)
|
||||
guard let id = UInt8(idStr)
|
||||
else { throw G3DbReaderError.badAttribute }
|
||||
return id
|
||||
}
|
||||
|
||||
return switch attrib
|
||||
{
|
||||
case "POSITION": .position
|
||||
case "NORMAL": .normal
|
||||
case "COLOR": .colour
|
||||
case "COLORPACKED": .colourPacked
|
||||
case "TANGENT": .tangent
|
||||
case "BINORMAL": .bitangent
|
||||
default:
|
||||
if attrib.starts(with: "TEXCOORD")
|
||||
{
|
||||
.texCoord(id: try getAttributeId(attrib, 8))
|
||||
}
|
||||
else if attrib.starts(with: "BLENDWEIGHT")
|
||||
{
|
||||
.blendWeight(id: try getAttributeId(attrib, 11))
|
||||
}
|
||||
else { throw G3DbReaderError.badAttribute }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum G3DPrimativeType
|
||||
{
|
||||
case invalid
|
||||
case triangles
|
||||
case lines
|
||||
case points
|
||||
case triangleStrip
|
||||
case lineStrip
|
||||
}
|
||||
|
||||
extension G3DPrimativeType
|
||||
{
|
||||
static func resolve(_ key: String) throws -> Self
|
||||
{
|
||||
switch key
|
||||
{
|
||||
case "TRIANGLES": .triangles
|
||||
case "LINES": .lines
|
||||
case "POINTS": .points
|
||||
case "TRIANGLE_STRIP": .triangleStrip
|
||||
case "LINE_STRIP": .lineStrip
|
||||
default: throw G3DbReaderError.invalidFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum G3DbReaderError: Error
|
||||
{
|
||||
case fileNotFound
|
||||
case versionMismatch
|
||||
case unsupportedFormat
|
||||
case invalidFormat
|
||||
case badAttribute
|
||||
case duplicateIDs
|
||||
}
|
50
Sources/JolkEngine/Loaders/NSImageLoader.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import AppKit
|
||||
|
||||
|
||||
enum ImageLoaderError: Error
|
||||
{
|
||||
case loadFailed(_ message: String)
|
||||
}
|
||||
|
||||
struct NSImageLoader: LoaderProtocol
|
||||
{
|
||||
typealias T = Image
|
||||
|
||||
func load(url: URL) -> T?
|
||||
{
|
||||
guard let image = try? NSImageLoader.loadImage(url: url) else { return nil }
|
||||
return image
|
||||
}
|
||||
|
||||
static func loadImage(url: URL) throws -> Image
|
||||
{
|
||||
try autoreleasepool
|
||||
{
|
||||
// Open as a Core Graphics image
|
||||
guard let nsImg = NSImage(contentsOf: url),
|
||||
let image = nsImg.cgImage(forProposedRect: nil, context: nil, hints: nil)
|
||||
else { throw ImageLoaderError.loadFailed("Failed to open image \"\(url.absoluteString)\"") }
|
||||
|
||||
// Convert to 8-bit ARGB (SRGB) w/ premultiplied alpha
|
||||
let alphaInfo = image.alphaInfo == .none ? CGImageAlphaInfo.noneSkipLast : CGImageAlphaInfo.premultipliedLast
|
||||
guard let colourspace = CGColorSpace(name: CGColorSpace.sRGB),
|
||||
let context = CGContext(data: nil,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: image.width * 4,
|
||||
space: colourspace,
|
||||
bitmapInfo: alphaInfo.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
|
||||
else { throw ImageLoaderError.loadFailed("Coudn't create graphics context") }
|
||||
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(image.height))
|
||||
context.concatenate(flipVertical)
|
||||
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
|
||||
|
||||
guard let data = context.data else { throw ImageLoaderError.loadFailed("what") }
|
||||
return Image(
|
||||
pixels: Data(bytes: data, count: 4 * image.width * image.height),
|
||||
width: image.width,
|
||||
height: image.height)
|
||||
}
|
||||
}
|
||||
}
|
92
Sources/JolkEngine/Loaders/ObjLoader.swift
Normal file
@ -0,0 +1,92 @@
|
||||
import Foundation
|
||||
import OrderedCollections
|
||||
|
||||
|
||||
/*
|
||||
extension ObjMaterial
|
||||
{
|
||||
func convert() -> Material
|
||||
{
|
||||
var m = Material()
|
||||
m.diffuse = self.diffuse.setAlpha(self.alpha)
|
||||
if ![ .colour, .lambert, .shadowOnly ].contains(self.model)
|
||||
{
|
||||
m.specular = self.specular
|
||||
m.specularExp = self.specularExp
|
||||
}
|
||||
return m
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public struct ObjLoader: LoaderProtocol
|
||||
{
|
||||
public typealias T = Mesh
|
||||
|
||||
public init() {}
|
||||
|
||||
public func load(url: URL) -> T?
|
||||
{
|
||||
try? Self.read(url: url)
|
||||
}
|
||||
|
||||
private static func read(url: URL) throws -> Mesh
|
||||
{
|
||||
return try read(model: try ObjReader.read(url: url))
|
||||
}
|
||||
|
||||
public static func read(model: ObjModel) throws -> Mesh
|
||||
{
|
||||
var subMeshes: OrderedDictionary<String, Mesh.SubMesh> = .init()
|
||||
var vertices = [Mesh.Vertex]()
|
||||
var indices = [UInt16]()
|
||||
|
||||
let readIndex =
|
||||
{ (v: ObjModel.Index) -> UInt16 in
|
||||
let vertex = Mesh.Vertex(
|
||||
position: model.positions[v.p],
|
||||
normal: model.normals[v.n],
|
||||
texCoord: model.texCoords[v.t])
|
||||
if let index = vertices.firstIndex(of: vertex)
|
||||
{
|
||||
indices.append(UInt16(index))
|
||||
return UInt16(index)
|
||||
}
|
||||
else
|
||||
{
|
||||
let index = UInt16(vertices.count)
|
||||
indices.append(index)
|
||||
vertices.append(vertex)
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
for mesh in model.objects
|
||||
{
|
||||
let start = indices.count
|
||||
for face: ObjModel.Face in mesh.value.faces
|
||||
{
|
||||
switch face
|
||||
{
|
||||
case .triangle(let v1, let v2, let v3):
|
||||
for v in [ v1, v2, v3 ] { _ = readIndex(v) }
|
||||
case .quad(let v1, let v2, let v3, let v4):
|
||||
let n1 = readIndex(v1)
|
||||
_ = readIndex(v2)
|
||||
indices.append(readIndex(v3))
|
||||
_ = readIndex(v4)
|
||||
indices.append(n1)
|
||||
case .ngon(_): fallthrough
|
||||
default: break
|
||||
}
|
||||
}
|
||||
let length = indices.count - start
|
||||
if length > 0
|
||||
{
|
||||
subMeshes[mesh.key] = .init(start: start, length: length)
|
||||
}
|
||||
}
|
||||
|
||||
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
|
||||
}
|
||||
}
|
372
Sources/JolkEngine/Loaders/ObjReader.swift
Normal file
@ -0,0 +1,372 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct ObjReader
|
||||
{
|
||||
public static func read(url: URL) throws -> ObjModel
|
||||
{
|
||||
var file = try ObjDocumentReader(filePath: url)
|
||||
|
||||
var model = ObjModel()
|
||||
var materials = Dictionary<String, ObjMaterial>()
|
||||
var name: String? = nil
|
||||
var object = ObjModel.Object()
|
||||
|
||||
file.string(tag: "mtllib") { s in
|
||||
let mats = try ObjMtlLoader.read(url: url.deletingLastPathComponent().appendingPathComponent(s[0]))
|
||||
materials.merge(mats, uniquingKeysWith: { (_, new) in new } )
|
||||
}
|
||||
file.string(tag: "o", count: 1) { s in
|
||||
if !object.faces.isEmpty
|
||||
{
|
||||
model.objects[name!] = object
|
||||
object = .init()
|
||||
}
|
||||
name = String(s[0])
|
||||
}
|
||||
file.float(tag: "v", count: 3) { v in model.positions.append(Vec3f(v[0], v[1], v[2])) }
|
||||
file.float(tag: "vn", count: 3) { v in model.normals.append(Vec3f(v[0], v[1], v[2])) }
|
||||
file.float(tag: "vt", count: 2) { v in model.texCoords.append(Vec2f(v[0], v[1])) }
|
||||
file.raw(tag: "f") { raw in
|
||||
let readIndex =
|
||||
{ (raw: Substring) in
|
||||
let face = raw.split(separator: "/")
|
||||
guard face.count == 3,
|
||||
let posIdx = Int(face[0]), let coordIdx = Int(face[1]), let normIdx = Int(face[2])
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
return ObjModel.Index(p: posIdx - 1, n: normIdx - 1, t: coordIdx - 1)
|
||||
}
|
||||
|
||||
if raw.count == 3
|
||||
{
|
||||
for raw in raw { _ = try readIndex(raw) }
|
||||
object.faces.append(.triangle(
|
||||
v1: try readIndex(raw[raw.startIndex]),
|
||||
v2: try readIndex(raw[raw.startIndex + 1]),
|
||||
v3: try readIndex(raw[raw.startIndex + 2])))
|
||||
}
|
||||
else if raw.count == 4
|
||||
{
|
||||
object.faces.append(.quad(
|
||||
v1: try readIndex(raw[raw.startIndex]),
|
||||
v2: try readIndex(raw[raw.startIndex + 1]),
|
||||
v3: try readIndex(raw[raw.startIndex + 2]),
|
||||
v4: try readIndex(raw[raw.startIndex + 3])))
|
||||
}
|
||||
else if raw.count >= 5
|
||||
{
|
||||
object.faces.append(.ngon(try raw.map { try readIndex($0) }))
|
||||
}
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}
|
||||
file.int(tag: "l") { i in object.faces.append(.line(p1: i[0], p2: i[1])) }
|
||||
try file.read()
|
||||
|
||||
if !object.faces.isEmpty
|
||||
{
|
||||
model.objects[name!] = object
|
||||
}
|
||||
return model
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ObjMtlLoader
|
||||
{
|
||||
static func read(url: URL) throws -> Dictionary<String, ObjMaterial>
|
||||
{
|
||||
var file = try ObjDocumentReader(filePath: url)
|
||||
|
||||
var materials = Dictionary<String, ObjMaterial>()
|
||||
var name: String = ""
|
||||
var mat: ObjMaterial? = nil
|
||||
|
||||
file.string(tag: "newmtl", count: 1)
|
||||
{ s in
|
||||
if mat != nil
|
||||
{
|
||||
materials[name] = mat!
|
||||
}
|
||||
mat = .init()
|
||||
name = String(s[0])
|
||||
}
|
||||
|
||||
file.preHandle { s in if s != "newmtl" && mat == nil { throw ObjLoaderError.unexpectedTag } }
|
||||
file.float(tag: "Ka", count: 3) { f in mat!.ambient = Colour(r: f[0], g: f[1], b: f[2]) } // "Ambient colour"
|
||||
file.float(tag: "Kd", count: 3) { f in mat!.diffuse = Colour(r: f[0], g: f[1], b: f[2]) } // "Diffuse colour"
|
||||
file.float(tag: "Ks", count: 3) { f in mat!.specular = Colour(r: f[0], g: f[1], b: f[2]) } // "Specular colour"
|
||||
file.float(tag: "Ns", count: 1) { f in mat!.specularExp = f[0] } // "Specular exponent/shininess"
|
||||
file.float(tag: "Ni", count: 1) { f in mat!.refraction = f[0] } // "Optical density/refraction index"
|
||||
file.float(tag: "d", count: 1) { f in mat!.alpha = f[0] } // "Dissolve" 0.0 = transparent, 1.0 = opaque
|
||||
file.float(tag: "Tr", count: 1) { f in mat!.alpha = 1.0 - f[0] } // "Transparent" 0.0 = opaque, 1.0 = transparent
|
||||
file.int(tag: "illum", count: 1) { i in mat!.model = try .resolve(i[0]) } // "Illumination model"
|
||||
file.textureMap(tag: "map_Ka") { s in } // "Ambient colourmap"
|
||||
file.textureMap(tag: "map_Kd") { t in mat!.diffuseMap = t } // "Albedo map"
|
||||
file.textureMap(tag: "map_Ks") { t in mat!.specularMap = t } // "Specular colourmap"
|
||||
file.textureMap(tag: "map_Ns") { t in mat!.specularExpMap = t } // "Specular exponent map"
|
||||
file.textureMap(tag: "map_d") { t in mat!.dissolveMap = t } // "Dissolve map"
|
||||
//file.string(tag: "map_Tr") { t in } // "Translucency map"
|
||||
for t in ["map_Bump", "bump"] { file.textureMap(tag: t) { t in mat!.bumpMap = t } } // "Bump map"
|
||||
file.textureMap(tag: "disp") { t in mat!.displacementMap = t } // "Displacement map"
|
||||
file.textureMap(tag: "decal") { t in mat!.decalMap = t } // ?
|
||||
// PBR extensions
|
||||
file.float(tag: "Pr", count: 1) { f in mat!.roughness = f[0] } // "Roughness"
|
||||
file.float(tag: "Pm", count: 1) { f in mat!.metallic = f[0] } // "Metallic"
|
||||
file.float(tag: "Ps", count: 1) { f in mat!.sheen = f[0] } // "Sheen"
|
||||
file.float(tag: "Pc", count: 1) { f in } // "Clearcoat thickness"
|
||||
file.float(tag: "Pcr", count: 1) { f in } // "Clearcoat roughness"
|
||||
file.float(tag: "Ke", count: 3) { f in } // "Emmission colour"
|
||||
file.float(tag: "aniso", count: 1) { f in } // "Anisotropy"
|
||||
file.float(tag: "anisor", count: 1) { f in } // "Anisotropy rotation"
|
||||
file.textureMap(tag: "map_Pr") { t in mat!.roughnessMap = t } // "Roughness texturemap"
|
||||
file.textureMap(tag: "map_Pm") { t in mat!.metallicMap = t } // "Metallic texturemap"
|
||||
file.textureMap(tag: "map_Ps") { t in mat!.sheenMap = t } // "Sheen texturemap"
|
||||
file.textureMap(tag: "map_Ke") { t in mat!.emmisionMap = t } // "Emmision texturemap"
|
||||
file.textureMap(tag: "norm") { t in mat!.normalMap = t } // "Normal map"
|
||||
|
||||
try file.read()
|
||||
|
||||
if let material = mat
|
||||
{
|
||||
materials[name] = material
|
||||
}
|
||||
return materials
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension ObjMaterial.IlluminationModel
|
||||
{
|
||||
static func resolve(_ id: Int) throws -> Self
|
||||
{
|
||||
switch id
|
||||
{
|
||||
case 0: .colour
|
||||
case 1: .lambert
|
||||
case 2: .blinnPhong
|
||||
case 3: .reflectionRaytrace
|
||||
case 4: .transparentGlassReflectionRaytrace
|
||||
case 5: .reflectionRaytraceFresnel
|
||||
case 6: .transparentRefractionReflectionRaytrace
|
||||
case 7: .transparentRefractionReflectionRaytraceFresnel
|
||||
case 8: .reflection
|
||||
case 9: .transparentGlassReflection
|
||||
case 10: .shadowOnly
|
||||
default: throw ObjLoaderError.badTagParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension ObjMaterial.TextureMap
|
||||
{
|
||||
static func parse(_ argstr: Substring) throws -> Self
|
||||
{
|
||||
var map = Self()
|
||||
var args = [Substring]()
|
||||
|
||||
let parseFlag =
|
||||
{ (flag: Flags) in
|
||||
let arg = args[0].lowercased()
|
||||
if arg == "on" { map.flags.insert(flag) }
|
||||
else if arg == "off" { map.flags.remove(flag) }
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}
|
||||
let parseInt =
|
||||
{ (arg: Int) in
|
||||
guard let result = Int(args[arg]) else { throw ObjLoaderError.badTagParameters }
|
||||
return result
|
||||
}
|
||||
let parseFloat =
|
||||
{ (arg: Int) in
|
||||
guard let result = Float(args[arg]) else { throw ObjLoaderError.badTagParameters }
|
||||
return result
|
||||
}
|
||||
let parseVector = { Vec3f(try parseFloat(0), try parseFloat(1), try parseFloat(2)) }
|
||||
|
||||
typealias Option = () throws -> Void
|
||||
let options: Dictionary<String, (Int, Option)> =
|
||||
[
|
||||
"blendu": (1, { try parseFlag(.blendHoriz) }),
|
||||
"blendv": (1, { try parseFlag(.blendVert) }),
|
||||
"bm": (1, { map.blendMul = try parseFloat(0) }),
|
||||
"cc": (1, { try parseFlag(.colourCorrection) }),
|
||||
"clamp": (1, { try parseFlag(.clamp) }),
|
||||
"imfchan": (1, { map.imfChan = try .resolve(args[0]) }),
|
||||
"mm": (2, { map.mmBaseGain = (try parseFloat(0), try parseFloat(1)) }),
|
||||
"o": (3, { map.offset = try parseVector() }),
|
||||
"s": (3, { map.scale = try parseVector() }),
|
||||
"t": (3, { map.turbulence = try parseVector() }),
|
||||
"texres": (1, { map.textureResolution = try parseInt(0) })
|
||||
]
|
||||
|
||||
var expectArgs = 0, option: Option? = nil
|
||||
var index = argstr.startIndex
|
||||
repeat
|
||||
{
|
||||
if expectArgs > 0, let callback = option
|
||||
{
|
||||
let start = index
|
||||
index = argstr[start...].firstIndex(where: \.isWhitespace) ?? argstr.endIndex
|
||||
args.append(argstr[start..<index])
|
||||
expectArgs -= 1
|
||||
if expectArgs == 0
|
||||
{
|
||||
try callback()
|
||||
option = nil
|
||||
}
|
||||
}
|
||||
else if argstr[index] == "-"
|
||||
{
|
||||
let start = argstr.index(after: index)
|
||||
index = argstr[start...].firstIndex(where: \.isWhitespace) ?? argstr.endIndex
|
||||
let name = String(argstr[start..<index])
|
||||
guard let params = options[name] else { throw ObjLoaderError.badTagParameters }
|
||||
(expectArgs, option) = params
|
||||
}
|
||||
else
|
||||
{
|
||||
break
|
||||
}
|
||||
index = argstr[index...].firstIndex { !$0.isWhitespace } ?? argstr.endIndex
|
||||
}
|
||||
while index < argstr.endIndex
|
||||
if index >= argstr.endIndex || expectArgs > 0 { throw ObjLoaderError.badTagParameters }
|
||||
|
||||
map.path = String(argstr[index...])
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension ObjMaterial.TextureMap.ImfChan
|
||||
{
|
||||
static func resolve(_ token: Substring) throws -> Self
|
||||
{
|
||||
switch token
|
||||
{
|
||||
case "r": .r
|
||||
case "g": .g
|
||||
case "b": .b
|
||||
case "m": .m
|
||||
case "l": .l
|
||||
case "z": .z
|
||||
default: throw ObjLoaderError.badTagParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ObjDocumentReader
|
||||
{
|
||||
private enum Handler
|
||||
{
|
||||
case string(closure: ([String]) throws -> Void, count: Int? = nil)
|
||||
case float(closure: ([Float]) throws -> Void, count: Int? = nil)
|
||||
case int(closure: ([Int]) throws -> Void, count: Int? = nil)
|
||||
case textureMap(closure: (ObjMaterial.TextureMap) throws -> Void)
|
||||
case raw(closure: ([Substring]) throws -> Void)
|
||||
}
|
||||
|
||||
private var file: TextFile
|
||||
typealias Callback = (Substring) throws -> Void
|
||||
private var _prehandler: Callback? = nil
|
||||
private var handlers = Dictionary<String, Handler>()
|
||||
|
||||
init(filePath: URL) throws
|
||||
{
|
||||
file = try TextFile(fileURL: filePath)
|
||||
}
|
||||
|
||||
mutating func string(tag: String, count: Int? = nil, closure: @escaping ([String]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .string(closure: closure, count: count)
|
||||
}
|
||||
|
||||
mutating func float(tag: String, count: Int? = nil, closure: @escaping ([Float]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .float(closure: closure, count: count)
|
||||
}
|
||||
|
||||
mutating func int(tag: String, count: Int? = nil, closure: @escaping ([Int]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .int(closure: closure, count: count)
|
||||
}
|
||||
|
||||
mutating func textureMap(tag: String, closure: @escaping (ObjMaterial.TextureMap) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .textureMap(closure: closure)
|
||||
}
|
||||
|
||||
mutating func raw(tag: String, closure: @escaping ([Substring]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .raw(closure: closure)
|
||||
}
|
||||
|
||||
mutating func preHandle(closure: @escaping Callback) { _prehandler = closure }
|
||||
|
||||
private func handle(_ handler: Handler, command: Substring, arg: Substring) throws
|
||||
{
|
||||
switch handler
|
||||
{
|
||||
case .string(let closure, let count):
|
||||
let args = arg.split(separator: " ")
|
||||
if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters }
|
||||
try closure(args.map({ String($0) }))
|
||||
case .float(let closure, let count):
|
||||
let args = arg.split(separator: " ")
|
||||
if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters }
|
||||
try closure(args.map(
|
||||
{
|
||||
if let value = Float($0) { return value }
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}))
|
||||
case .int(let closure, let count):
|
||||
let args = arg.split(separator: " ")
|
||||
if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters }
|
||||
try closure(args.map(
|
||||
{
|
||||
if let value = Int($0) { return value }
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}))
|
||||
case .textureMap(let closure):
|
||||
try closure(ObjMaterial.TextureMap.parse(arg))
|
||||
case .raw(let closure):
|
||||
try closure(arg.split(separator: " "))
|
||||
}
|
||||
}
|
||||
|
||||
private func parseLine(_ line: String) throws -> (Substring, Substring)?
|
||||
{
|
||||
// Trim comment if present
|
||||
var trimmed = if let index = line.firstIndex(of: "#") { line[..<index] } else { line[...] }
|
||||
|
||||
// Trim leading and trailing whitespace
|
||||
guard let start = trimmed.firstIndex(where: { !$0.isWhitespace }),
|
||||
let end = trimmed.lastIndex(where: { !$0.isWhitespace })
|
||||
else { return nil }
|
||||
trimmed = trimmed[start...end]
|
||||
|
||||
// Split command & rest of string as arguments
|
||||
guard let cmdEnd = trimmed.firstIndex(where: \.isWhitespace),
|
||||
let argStart = trimmed.suffix(from: trimmed.index(after: cmdEnd)).firstIndex(where: { !$0.isWhitespace })
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
return (trimmed.prefix(upTo: cmdEnd), trimmed.suffix(from: argStart))
|
||||
}
|
||||
|
||||
func read() throws
|
||||
{
|
||||
for line in file.lines
|
||||
{
|
||||
if !line.isEmpty,
|
||||
let (cmd, argstr) = try parseLine(line),
|
||||
let handler = handlers[String(cmd)]
|
||||
{
|
||||
if let prehandler = _prehandler { try prehandler(cmd) }
|
||||
try handle(handler, command: cmd, arg: argstr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum ObjLoaderError: Error
|
||||
{
|
||||
case badTagParameters
|
||||
case unexpectedTag
|
||||
}
|
291
Sources/JolkEngine/Maths.swift
Normal file
@ -0,0 +1,291 @@
|
||||
import Foundation
|
||||
import simd
|
||||
|
||||
|
||||
public extension FloatingPoint
|
||||
{
|
||||
@inline(__always) var saturate: Self { min(max(self , 0), 1) }
|
||||
|
||||
@inline(__always) static func lerp(_ a: Self, _ b: Self, _ x: Self) -> Self { a * (1 - x) + b * x }
|
||||
|
||||
@inline(__always) static func deg(fromRad: Self) -> Self { fromRad * (180 / Self.pi) }
|
||||
@inline(__always) static func rad(fromDeg: Self) -> Self { fromDeg * (Self.pi / 180) }
|
||||
|
||||
fileprivate func axisDeadzone(_ min: Self, _ max: Self) -> Self
|
||||
{
|
||||
let xabs = abs(self)
|
||||
return if xabs <= min { 0 }
|
||||
else if xabs >= max { Self(signOf: self, magnitudeOf: 1) }
|
||||
else { Self(signOf: self, magnitudeOf: xabs - min) / (max - min) }
|
||||
}
|
||||
}
|
||||
|
||||
public extension SIMD2 where Scalar: FloatingPoint
|
||||
{
|
||||
@inline(__always) var len2: Scalar { x * x + y * y }
|
||||
//@inline(__always) var len2: Scalar { simd_dot(self, self) }
|
||||
@inline(__always) var len: Scalar { len2.squareRoot() }
|
||||
@inline(__always) var normalised: Self { self / len }
|
||||
|
||||
@inline(__always) func dot(_ b: Self) -> Scalar { x * b.x + y * b.y }
|
||||
@inline(__always) func reflect(_ n: Self) -> Self { self - (n * 2 * self.dot(n)) }
|
||||
@inline(__always) func project(_ n: Self) -> Self { n * self.dot(n) }
|
||||
|
||||
@inline(__always) func cross(_ b: Self) -> Scalar { x * b.y - y * b.x }
|
||||
|
||||
@inline(__always) func lerp(_ b: Self, _ x: Scalar) -> Self
|
||||
{
|
||||
let invX = 1 - x
|
||||
return Self(self.x * invX + b.x * x, self.y * invX + b.y * x)
|
||||
}
|
||||
|
||||
func cardinalDeadzone(min: Scalar, max: Scalar) -> Self
|
||||
{
|
||||
Self(self.x.axisDeadzone(min, max), self.y.axisDeadzone(min, max))
|
||||
}
|
||||
|
||||
func radialDeadzone(min: Scalar, max: Scalar) -> Self
|
||||
{
|
||||
let magnitude = self.len
|
||||
if magnitude == .zero || magnitude < min { return Self.zero }
|
||||
if magnitude > max { return self / magnitude }
|
||||
let rescale = (magnitude - min) / (max - min)
|
||||
return self / magnitude * rescale
|
||||
}
|
||||
}
|
||||
|
||||
public extension SIMD3 where Scalar: FloatingPoint
|
||||
{
|
||||
@inline(__always) static var X: Self { Self(1, 0, 0) }
|
||||
@inline(__always) static var Y: Self { Self(0, 1, 0) }
|
||||
@inline(__always) static var Z: Self { Self(0, 0, 1) }
|
||||
|
||||
@inline(__always) static var up: Self { Y }
|
||||
@inline(__always) static var down: Self { -Y }
|
||||
@inline(__always) static var left: Self { -X }
|
||||
@inline(__always) static var right: Self { X }
|
||||
@inline(__always) static var forward: Self { Z }
|
||||
@inline(__always) static var back: Self { -Z }
|
||||
|
||||
@inline(__always) var len2: Scalar { x * x + y * y + z * z }
|
||||
@inline(__always) var len: Scalar { len2.squareRoot() }
|
||||
@inline(__always) var normalised: Self { self / len }
|
||||
|
||||
@inline(__always) mutating func normalise() { self /= len }
|
||||
|
||||
@inline(__always) func lerp(_ b: Self, _ x: Scalar) -> Self
|
||||
{
|
||||
let invX = 1 - x
|
||||
return Self(self.x * invX + b.x * x, self.y * invX + b.y * x, self.z * invX + b.z * x)
|
||||
}
|
||||
|
||||
@inline(__always) func dot(_ b: Self) -> Scalar { x * b.x + y * b.y + z * b.z }
|
||||
@inline(__always) func cross(_ b: Self) -> Self { Self(y * b.z - z * b.y, z * b.x - x * b.z, x * b.y - y * b.x) }
|
||||
|
||||
@inline(__always) func project(_ n: Self) -> Self { n * self.dot(n) }
|
||||
}
|
||||
|
||||
public extension SIMD4 where Scalar: FloatingPoint
|
||||
{
|
||||
@inline(__always) static var X: Self { Self(1, 0, 0, 0) }
|
||||
@inline(__always) static var Y: Self { Self(0, 1, 0, 0) }
|
||||
@inline(__always) static var Z: Self { Self(0, 0, 1, 0) }
|
||||
@inline(__always) static var W: Self { Self(0, 0, 0, 1) }
|
||||
}
|
||||
|
||||
public extension simd_float4x4
|
||||
{
|
||||
@inline(__always) static var identity: Self { Self(diagonal: .one) }
|
||||
|
||||
@inline(__always) static func translate(_ v: Vec3f) -> Self
|
||||
{
|
||||
Self(
|
||||
.init( 1, 0, 0, 0),
|
||||
.init( 0, 1, 0 ,0),
|
||||
.init( 0, 0, 1, 0),
|
||||
.init(v.x, v.y, v.z, 1))
|
||||
}
|
||||
|
||||
@inline(__always) static func scale(_ v: Vec3f) -> Self
|
||||
{
|
||||
Self(
|
||||
.init(v.x, 0, 0, 0),
|
||||
.init( 0, v.y, 0, 0),
|
||||
.init( 0, 0, v.z, 0),
|
||||
.init( 0, 0, 0, 1))
|
||||
}
|
||||
|
||||
@inline(__always) static func scale(scalar v: Float) -> Self
|
||||
{
|
||||
Self(
|
||||
.init(v, 0, 0, 0),
|
||||
.init(0, v, 0, 0),
|
||||
.init(0, 0, v, 0),
|
||||
.init(0, 0, 0, 1))
|
||||
}
|
||||
|
||||
static func rotate(axis v: Vec3f, angle theta: Float) -> Self
|
||||
{
|
||||
//FIXME: THIS IS FUCKED UP FOR EVERYTHING OTHER THAN X AXIS ROTATION LOL
|
||||
/*
|
||||
let vv = v * v
|
||||
let xy = v.x * v.y, xz = v.x * v.z, yz = v.y * v.z
|
||||
let ts = sin(angle), tc = cos(angle)
|
||||
return Self(
|
||||
.init(
|
||||
vv.x + (tc * (1 - vv.x)),
|
||||
(xz - (tc * xy)) + (ts * v.z),
|
||||
(xz - (tc * xz)) - (ts * v.y), 0),
|
||||
.init(
|
||||
(xy - (tc * xy)) - (ts * v.z),
|
||||
vv.y + (tc * (1 - vv.y)),
|
||||
(xz - (tc * xz)) + (ts * v.x), 0),
|
||||
.init(
|
||||
(xz - (tc * xz)) + (ts * v.y),
|
||||
(xz - (tc * xz)) - (ts * v.y),
|
||||
vv.z + (tc * (1 - vv.z)), 1),
|
||||
.init(0, 0, 0, 1))
|
||||
*/
|
||||
let vxx = v.x * v.x, vxy = v.x * v.y, vxz = v.x * v.z
|
||||
let vyy = v.y * v.y, vyz = v.y * v.z
|
||||
let vzz = v.z * v.z
|
||||
let ts = sin(theta), tc = cos(theta)
|
||||
let ic = 1 - tc
|
||||
return Self(
|
||||
.init(
|
||||
ic * vxx + tc,
|
||||
ic * vxy - v.z * ts,
|
||||
ic * vxz + v.z * ts,
|
||||
0),
|
||||
.init(
|
||||
ic * vxy + v.z * ts,
|
||||
ic * vyy + tc,
|
||||
ic * vyz - v.x * ts,
|
||||
0),
|
||||
.init(
|
||||
ic * vxz - v.y * ts,
|
||||
ic * vyz + v.x * ts,
|
||||
ic * vzz + tc,
|
||||
0),
|
||||
.init(0, 0, 0, 1))
|
||||
}
|
||||
|
||||
@inline(__always) static func rotate(yawPitch: Vec2f) -> Self { return rotate(yaw: yawPitch.x, pitch: yawPitch.y) }
|
||||
|
||||
static func rotate(yaw ytheta: Float, pitch xtheta: Float) -> Self
|
||||
{
|
||||
let xc = cos(xtheta), xs = sin(xtheta)
|
||||
let yc = cos(ytheta), ys = sin(ytheta)
|
||||
|
||||
return Self(
|
||||
.init(yc, ys * xs, -ys * xc, 0),
|
||||
.init( 0, xc, xs, 0),
|
||||
.init(ys, yc * -xs, yc * xc, 0),
|
||||
.init( 0, 0, 0, 1))
|
||||
}
|
||||
|
||||
static func rotate(yaw ytheta: Float, pitch xtheta: Float, roll ztheta: Float) -> Self
|
||||
{
|
||||
//FIXME: this doesn't null against control
|
||||
let xc = cos(xtheta), xs = sin(xtheta)
|
||||
let yc = cos(ytheta), ys = sin(ytheta)
|
||||
let zc = cos(ztheta), zs = sin(ztheta)
|
||||
|
||||
let ysxs = ys * xs, ycxs = yc * xs
|
||||
|
||||
let result = Mat4f(
|
||||
.init(yc * zc + ysxs * zs, yc * -zs + ysxs * zc, -ys * xc, 0),
|
||||
.init( xc * zs, xc * zc, xs, 0),
|
||||
.init(ys * zc - ycxs * zs, ys * -zs - ycxs * zc, yc * xc, 0),
|
||||
.init( 0, 0, 0, 1))
|
||||
let shouldBe = .rotate(z: ztheta) * .rotate(x: xtheta) * .rotate(y: ytheta)
|
||||
|
||||
let epsilon: Float = .ulpOfOne
|
||||
if (result != shouldBe)
|
||||
{
|
||||
assert(abs(result[0][0] - shouldBe[0][0]) <= epsilon) // epsilon
|
||||
assert(result[1][0] == shouldBe[1][0])
|
||||
assert(abs(result[2][0] - shouldBe[2][0]) <= epsilon) // epsilon
|
||||
assert(result[3][0] == shouldBe[3][0])
|
||||
assert(abs(result[0][1] - shouldBe[0][1]) <= epsilon) // epsilon
|
||||
assert(result[1][1] == shouldBe[1][1])
|
||||
assert(abs(result[2][1] - shouldBe[2][1]) <= epsilon) // epsilon
|
||||
assert(result[3][1] == shouldBe[3][1])
|
||||
assert(result[0][2] == shouldBe[0][2])
|
||||
assert(result[1][2] == shouldBe[1][2])
|
||||
assert(result[2][2] == shouldBe[2][2])
|
||||
assert(result[3][2] == shouldBe[3][2])
|
||||
assert(result[0][3] == shouldBe[0][3])
|
||||
assert(result[1][3] == shouldBe[1][3])
|
||||
assert(result[2][3] == shouldBe[2][3])
|
||||
assert(result[3][3] == shouldBe[3][3])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@inline(__always) static func rotate(x theta: Float) -> Self
|
||||
{
|
||||
let c = cos(theta), s = sin(theta)
|
||||
return Self(
|
||||
.init(1, 0, 0, 0),
|
||||
.init(0, c, s, 0),
|
||||
.init(0, -s, c, 0),
|
||||
.init(0, 0, 0, 1))
|
||||
}
|
||||
|
||||
@inline(__always) static func rotate(y theta: Float) -> Self
|
||||
{
|
||||
let c = cos(theta), s = sin(theta)
|
||||
return Self(
|
||||
.init(c, 0, -s, 0),
|
||||
.init(0, 1, 0, 0),
|
||||
.init(s, 0, c, 0),
|
||||
.init(0, 0, 0, 1))
|
||||
}
|
||||
|
||||
@inline(__always) static func rotate(z theta: Float) -> Self
|
||||
{
|
||||
let c = cos(theta), s = sin(theta)
|
||||
return Self(
|
||||
.init(c, -s, 0, 0),
|
||||
.init(s, c, 0, 0),
|
||||
.init(0, 0, 1, 0),
|
||||
.init(0, 0, 0, 1))
|
||||
}
|
||||
|
||||
static func perspective(fovY: Float, aspect: Float, zNear: Float, zFar: Float) -> Self
|
||||
{
|
||||
let h = 1 / tanf(fovY * 0.5)
|
||||
let w = h / aspect
|
||||
let invClipRange = 1 / (zFar - zNear)
|
||||
let z = -(zFar + zNear) * invClipRange
|
||||
let z2 = -(2 * zFar * zNear) * invClipRange
|
||||
return simd_matrix(
|
||||
.init(w, 0, 0, 0),
|
||||
.init(0, h, 0, 0),
|
||||
.init(0, 0, z, -1),
|
||||
.init(0, 0, z2, 0))
|
||||
}
|
||||
|
||||
static func lookAt(from: Vec3f = .zero, to: Vec3f, up: Vec3f = .up) -> Self
|
||||
{
|
||||
let forward = (to - from).normalised
|
||||
let bitangent = forward.cross(up).normalised
|
||||
let tangent = bitangent.cross(forward).normalised
|
||||
let normal = -forward
|
||||
return simd_matrix(
|
||||
.init(bitangent.x, tangent.x, normal.x, 0.0),
|
||||
.init(bitangent.y, tangent.y, normal.y, 0.0),
|
||||
.init(bitangent.z, tangent.z, normal.z, 0.0),
|
||||
.init( 0.0, 0.0, 0.0, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public typealias Vec2f = SIMD2<Float>
|
||||
public typealias Vec2d = SIMD2<Double>
|
||||
public typealias Vec3f = SIMD3<Float>
|
||||
public typealias Vec3d = SIMD3<Double>
|
||||
public typealias Vec4f = SIMD4<Float>
|
||||
public typealias Vec4d = SIMD4<Double>
|
||||
public typealias Mat4f = simd_float4x4
|
81
Sources/JolkEngine/Renderer/Environment.swift
Normal file
@ -0,0 +1,81 @@
|
||||
public enum Fog
|
||||
{
|
||||
public enum Mode
|
||||
{
|
||||
case distance
|
||||
case depth
|
||||
}
|
||||
|
||||
public enum RangeType
|
||||
{
|
||||
case linear
|
||||
case smooth
|
||||
}
|
||||
|
||||
public enum FactorType
|
||||
{
|
||||
case exp
|
||||
case exp2
|
||||
}
|
||||
|
||||
case none
|
||||
case range(colour: Colour, mode: Mode, type: RangeType, start: Float, end: Float)
|
||||
case factor(colour: Colour, mode: Mode, type: FactorType, density: Float)
|
||||
}
|
||||
|
||||
public enum Light
|
||||
{
|
||||
case directional(colour: Colour, direction: Vec3f)
|
||||
case point(colour: Colour, position: Vec3f, intensity: Float)
|
||||
}
|
||||
|
||||
public struct Environment
|
||||
{
|
||||
public var fog: Fog = .none
|
||||
public var ambient: Colour = .black
|
||||
public var lights: [Light] = .init()
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(fog: Fog, ambient: Colour, lights: [Light])
|
||||
{
|
||||
self.fog = fog
|
||||
self.ambient = ambient
|
||||
self.lights = lights
|
||||
}
|
||||
}
|
||||
|
||||
public extension Environment
|
||||
{
|
||||
enum FogFalloff
|
||||
{
|
||||
case range(type: Fog.RangeType, start: Float, end: Float)
|
||||
case factor(type: Fog.FactorType, density: Float)
|
||||
}
|
||||
|
||||
mutating func setFog(colour: Colour, mode: Fog.Mode, falloff: FogFalloff)
|
||||
{
|
||||
fog = switch falloff
|
||||
{
|
||||
case .range(let type, let start, let end):
|
||||
.range(colour: colour, mode: mode, type: type, start: start, end: end)
|
||||
case .factor(let type, let density):
|
||||
.factor(colour: colour, mode: mode, type: type, density: density)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func addAmbientLight(colour: Colour)
|
||||
{
|
||||
ambient = colour
|
||||
}
|
||||
|
||||
mutating func addDirectionalLight(direction: Vec3f, colour: Colour)
|
||||
{
|
||||
lights.append(.directional(colour: colour, direction: direction))
|
||||
}
|
||||
|
||||
mutating func addPointLight(position: Vec3f, colour: Colour, intensity: Float)
|
||||
{
|
||||
lights.append(.point(colour: colour, position: position, intensity: intensity))
|
||||
}
|
||||
}
|
20
Sources/JolkEngine/Renderer/Material.swift
Normal file
@ -0,0 +1,20 @@
|
||||
public struct Material
|
||||
{
|
||||
public var id: String
|
||||
public var diffuse: Colour, specular: Colour
|
||||
public var specularExp: Float
|
||||
public var texture: RenderTexture2D
|
||||
|
||||
public init(id: String = "",
|
||||
diffuse: Colour = .white,
|
||||
specular: Colour = .zero,
|
||||
gloss: Float = 0,
|
||||
texture: RenderTexture2D = .empty)
|
||||
{
|
||||
self.id = id
|
||||
self.diffuse = diffuse
|
||||
self.specular = specular
|
||||
self.specularExp = gloss
|
||||
self.texture = texture
|
||||
}
|
||||
}
|
440
Sources/JolkEngine/Renderer/OpenGL.swift
Normal file
@ -0,0 +1,440 @@
|
||||
//#set OPENGL3
|
||||
|
||||
import Foundation
|
||||
import SDL2
|
||||
#if OPENGL3
|
||||
import OpenGL
|
||||
#else
|
||||
import OpenGL.GL3
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
class OpenGL: Renderer
|
||||
{
|
||||
private let srgb = true
|
||||
|
||||
private var glCtx: SDL_GLContext? = nil
|
||||
private var state = OpenGLState()
|
||||
|
||||
struct Version { let major, minor: Int32 }
|
||||
let glVersion: Version
|
||||
|
||||
init(version: Version)
|
||||
{
|
||||
self.glVersion = version
|
||||
}
|
||||
|
||||
func create(sdlWindow: OpaquePointer) throws
|
||||
{
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, Int32(SDL_GL_CONTEXT_PROFILE_COMPATIBILITY.rawValue))
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glVersion.major)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glVersion.minor)
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1)
|
||||
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0)
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0)
|
||||
|
||||
if let context = SDL_GL_CreateContext(sdlWindow) { glCtx = context }
|
||||
else { throw RendererError.sdlError(message: "SDL_GL_CreateContext: \(String(cString: SDL_GetError()))") }
|
||||
|
||||
guard SDL_GL_MakeCurrent(sdlWindow, glCtx) == 0
|
||||
else { throw RendererError.sdlError(message: "SDL_GL_MakeCurrent: \(String(cString: SDL_GetError()))") }
|
||||
|
||||
state.enable([.texture2D, .cullFace, .depthTest, .rescaleNormal, .colourMaterial])
|
||||
if srgb { state.enable(.frameBufferSrgb) }
|
||||
state.cullFace = .back
|
||||
state.clearDepth = 1
|
||||
state.depthFunc = .less
|
||||
state.setHint(target: .fog, hint: .dontCare)
|
||||
}
|
||||
|
||||
func delete()
|
||||
{
|
||||
SDL_GL_DeleteContext(glCtx)
|
||||
}
|
||||
|
||||
func resize(width: Int32, height: Int32)
|
||||
{
|
||||
glViewport(0, 0, width, height)
|
||||
}
|
||||
|
||||
func newFrame()
|
||||
{
|
||||
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
|
||||
}
|
||||
|
||||
private var _clearColour = Colour.black
|
||||
var clearColour: Colour
|
||||
{
|
||||
get { _clearColour }
|
||||
set(newColour)
|
||||
{
|
||||
|
||||
state.clearColour(srgb ? newColour.linear : newColour)
|
||||
_clearColour = newColour
|
||||
}
|
||||
}
|
||||
|
||||
func setVsync(mode: VSyncMode) throws
|
||||
{
|
||||
guard SDL_GL_SetSwapInterval(mode.sdlInterval) == 0
|
||||
else { throw RendererError.sdlError(message: "SDL_GL_SetSwapInterval: \(String(cString: SDL_GetError()))") }
|
||||
}
|
||||
|
||||
func createMesh(mesh: Mesh) throws -> RenderMesh
|
||||
{
|
||||
var buffers = [GLuint](repeating: 0, count: 2)
|
||||
buffers.withUnsafeMutableBufferPointer
|
||||
{
|
||||
glGenBuffers(2, $0.baseAddress!)
|
||||
}
|
||||
|
||||
state.arrayBuffer = buffers[0]
|
||||
state.elementArrayBuffer = buffers[1]
|
||||
|
||||
glBufferData(GLenum(GL_ARRAY_BUFFER),
|
||||
MemoryLayout<Mesh.Vertex>.stride * mesh.vertices.count,
|
||||
mesh.vertices, GLenum(GL_STATIC_DRAW))
|
||||
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
|
||||
MemoryLayout<Mesh.Index>.stride * mesh.indices.count,
|
||||
mesh.indices, GLenum(GL_STATIC_DRAW))
|
||||
|
||||
state.elementArrayBuffer = OpenGLState.defaultBuffer
|
||||
state.arrayBuffer = OpenGLState.defaultBuffer
|
||||
|
||||
var subMeshes = [Mesh.SubMesh]()
|
||||
if mesh.subMeshes.isEmpty
|
||||
{
|
||||
subMeshes.append(Mesh.SubMesh(start: 0, length: mesh.indices.count))
|
||||
}
|
||||
else
|
||||
{
|
||||
for subMesh in mesh.subMeshes
|
||||
{
|
||||
if ["Collision", "Collision3D"].contains(subMesh.key) { continue }
|
||||
subMeshes.append(Mesh.SubMesh(
|
||||
start: subMesh.value.start,
|
||||
length: subMesh.value.length))
|
||||
}
|
||||
}
|
||||
|
||||
return RenderMesh(
|
||||
vbo: Int(buffers[0]),
|
||||
ibo: Int(buffers[1]),
|
||||
subMeshes: subMeshes)
|
||||
}
|
||||
|
||||
func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D
|
||||
{
|
||||
try createTexture(data: data, width: width, height: height, filter: .linear, mipMode: .off)
|
||||
}
|
||||
|
||||
func createTexture(data: UnsafeRawPointer, width: Int, height: Int,
|
||||
filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D
|
||||
{
|
||||
let min: Int32 = switch mipMode
|
||||
{
|
||||
case .off: filter.gl
|
||||
case .nearest: filter.glMip
|
||||
case .linear: filter.glLinearMip
|
||||
}
|
||||
return RenderTexture2D(try loadTexture(data: data,
|
||||
width: GLsizei(width), height: GLsizei(height),
|
||||
minFilter: min, magFilter: filter.gl))
|
||||
}
|
||||
|
||||
private func loadTexture(
|
||||
data: UnsafeRawPointer,
|
||||
width: GLsizei, height: GLsizei,
|
||||
minFilter: Int32 = GL_LINEAR, magFilter: Int32 = GL_LINEAR,
|
||||
wrap: Int32 = GL_REPEAT) throws -> GLuint
|
||||
{
|
||||
// Create & upload raw bytes as texture
|
||||
var texId: GLuint = 0
|
||||
glGenTextures(1, &texId)
|
||||
state.bindTexture2D(active: 0, texture: texId)
|
||||
state.texture2DParameter(active: 0, param: .minFilter, int: minFilter)
|
||||
state.texture2DParameter(active: 0, param: .magFilter, int: magFilter)
|
||||
state.texture2DParameter(active: 0, param: .wrapS, int: wrap)
|
||||
state.texture2DParameter(active: 0, param: .wrapT, int: wrap)
|
||||
let format: GLint = srgb ? GL_SRGB8 : GL_RGBA
|
||||
glTexImage2D(GLenum(GL_TEXTURE_2D),
|
||||
0, format, width, height, 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
|
||||
|
||||
// Generate mipmaps if needed
|
||||
if [GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST]
|
||||
.contains(minFilter) { glGenerateMipmap(GLenum(GL_TEXTURE_2D)) }
|
||||
|
||||
state.bindTexture2D(active: 0, texture: OpenGLState.defaultTexture)
|
||||
|
||||
return texId
|
||||
}
|
||||
|
||||
|
||||
func deleteMesh(_ mesh: RenderMesh)
|
||||
{
|
||||
var buffers = [GLuint](repeating: 0, count: 2)
|
||||
buffers[0] = GLuint(mesh.iboHnd)
|
||||
buffers[1] = GLuint(mesh.vboHnd)
|
||||
|
||||
glDeleteBuffers(1, buffers)
|
||||
}
|
||||
|
||||
func deleteTexture(_ texture: RenderTexture2D)
|
||||
{
|
||||
var texId = GLuint(texture.id)
|
||||
glDeleteTextures(1, &texId)
|
||||
}
|
||||
|
||||
|
||||
func setProjection(matrix: Mat4f)
|
||||
{
|
||||
state.matrixMode = .projection
|
||||
loadMatrix(matrix)
|
||||
}
|
||||
|
||||
func setView(matrix: Mat4f)
|
||||
{
|
||||
state.matrixMode = .modelView
|
||||
loadMatrix(matrix)
|
||||
}
|
||||
|
||||
|
||||
func setMaterial(_ mat: Material)
|
||||
{
|
||||
glColor4f(mat.diffuse.r, mat.diffuse.g, mat.diffuse.b, mat.diffuse.a)
|
||||
state.bindTexture2D(active: 0, texture: mat.texture.isValid ? mat.texture.id : 0)
|
||||
}
|
||||
|
||||
|
||||
private func draw(subMesh: Mesh.SubMesh)
|
||||
{
|
||||
glDrawElements(
|
||||
GLenum(GL_TRIANGLES),
|
||||
GLsizei(subMesh.length),
|
||||
GLenum(GL_UNSIGNED_SHORT),
|
||||
.init(bitPattern: MemoryLayout<Mesh.Index>.stride * subMesh.start));
|
||||
}
|
||||
|
||||
private func draw(mesh: RenderMesh)
|
||||
{
|
||||
state.enableClient([ .vertex, .normal, .textureCoord ])
|
||||
|
||||
state.arrayBuffer = UInt32(mesh.vboHnd)
|
||||
state.elementArrayBuffer = UInt32(mesh.iboHnd)
|
||||
|
||||
let stride = GLsizei(MemoryLayout<Mesh.Vertex>.stride)
|
||||
glVertexPointer(3, GLenum(GL_FLOAT), stride,
|
||||
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.position)!))
|
||||
glNormalPointer(GLenum(GL_FLOAT), stride,
|
||||
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.normal)!))
|
||||
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
|
||||
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.texCoord)!))
|
||||
|
||||
for subMesh in mesh.subMeshes
|
||||
{
|
||||
draw(subMesh: subMesh)
|
||||
}
|
||||
|
||||
state.elementArrayBuffer = OpenGLState.defaultBuffer
|
||||
state.arrayBuffer = OpenGLState.defaultBuffer
|
||||
|
||||
state.disableClient([ .vertex, .normal, .textureCoord ])
|
||||
}
|
||||
|
||||
func draw(mesh: RenderMesh, model: Mat4f, environment env: Environment)
|
||||
{
|
||||
if (mesh.subMeshes.isEmpty) { return }
|
||||
|
||||
applyEnvironment(env)
|
||||
state.matrixMode = .modelView
|
||||
glPushMatrix()
|
||||
mulMatrix(model)
|
||||
draw(mesh: mesh)
|
||||
glPopMatrix()
|
||||
}
|
||||
|
||||
func draw(mesh: RenderMesh, environment env: Environment)
|
||||
{
|
||||
if (mesh.subMeshes.isEmpty) { return }
|
||||
|
||||
applyEnvironment(env)
|
||||
draw(mesh: mesh)
|
||||
}
|
||||
|
||||
private func loadMatrix(_ matrix: Mat4f)
|
||||
{
|
||||
withUnsafePointer(to: matrix.columns)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
|
||||
{ (values: UnsafePointer<GLfloat>) in
|
||||
glLoadMatrixf(values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mulMatrix(_ matrix: Mat4f)
|
||||
{
|
||||
withUnsafePointer(to: matrix.columns)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
|
||||
{ (values: UnsafePointer<GLfloat>) in
|
||||
glMultMatrixf(values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func applyEnvironment(_ env: Environment)
|
||||
{
|
||||
let coordSource =
|
||||
{ (mode: Fog.Mode) -> OpenGLState.FogCoordinateSource in
|
||||
switch mode
|
||||
{
|
||||
case .depth: .fragmentDepth
|
||||
case .distance: .coordinate
|
||||
}
|
||||
}
|
||||
|
||||
switch env.fog
|
||||
{
|
||||
case .none:
|
||||
state.disable(.fog)
|
||||
case .range(let colour, let mode, _, let start, let end):
|
||||
state.fogMode = .linear
|
||||
state.fogColour = srgb ? colour.linear : colour
|
||||
state.fogCoodinateSource = coordSource(mode)
|
||||
state.fogStart = start
|
||||
state.fogEnd = end
|
||||
state.enable(.fog)
|
||||
break
|
||||
case .factor(let colour, let mode, let type, let density):
|
||||
state.fogMode = switch type
|
||||
{
|
||||
case .exp: .exp
|
||||
case .exp2: .exp2
|
||||
}
|
||||
state.fogColour = srgb ? colour.linear : colour
|
||||
state.fogCoodinateSource = coordSource(mode)
|
||||
state.fogDensity = density
|
||||
state.enable(.fog)
|
||||
break
|
||||
}
|
||||
|
||||
state.lightModelAmbient = env.ambient
|
||||
|
||||
let lightCaps: [OpenGLState.Capability] = [
|
||||
.light0, .light1, .light2, .light3,
|
||||
.light4, .light5, .light6, .light7 ]
|
||||
if !env.lights.isEmpty
|
||||
{
|
||||
state.enable(.lighting)
|
||||
for i in 0..<8
|
||||
{
|
||||
if i < env.lights.count
|
||||
{
|
||||
switch env.lights[i]
|
||||
{
|
||||
case .directional(let colour, let direction):
|
||||
state.lightPosition[i] = Vec4f(direction, 0)
|
||||
state.lightDiffuse[i] = srgb ? colour.linear : colour
|
||||
case .point(let colour, let position, let intensity):
|
||||
state.lightPosition[i] = Vec4f(position, 1)
|
||||
state.lightDiffuse[i] = srgb ? colour.linear : colour
|
||||
state.lightConstantAttenuation[i] = 0
|
||||
state.lightLinearAttenuation[i] = intensity
|
||||
state.lightQuadraticAttenuation[i] = 0
|
||||
}
|
||||
state.enable(lightCaps[i])
|
||||
}
|
||||
else
|
||||
{
|
||||
state.disable(lightCaps[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.disable(.lighting)
|
||||
for cap in lightCaps { state.disable(cap) }
|
||||
}
|
||||
}
|
||||
|
||||
func drawGizmos(lines: [Line])
|
||||
{
|
||||
state.disable([ .texture2D, .depthTest ])
|
||||
var gizmoEnv = Environment()
|
||||
gizmoEnv.fog = .none
|
||||
applyEnvironment(gizmoEnv)
|
||||
|
||||
glBegin(GLenum(GL_LINES))
|
||||
for line in lines
|
||||
{
|
||||
let colour = srgb ? line.colour.linear : line.colour
|
||||
glColor4f(colour.r, colour.g, colour.b, colour.a)
|
||||
glVertex3f(line.from.x, line.from.y, line.from.z)
|
||||
glVertex3f(line.to.x, line.to.y, line.to.z)
|
||||
}
|
||||
glEnd()
|
||||
state.enable([ .texture2D, .depthTest ])
|
||||
}
|
||||
}
|
||||
|
||||
extension VSyncMode
|
||||
{
|
||||
fileprivate var sdlInterval: Int32
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .off: 0
|
||||
case .on: 1
|
||||
case .adaptive: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterMode
|
||||
{
|
||||
fileprivate var gl: Int32
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .point: GL_NEAREST
|
||||
case .linear: GL_LINEAR
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var glMip: Int32
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .point: GL_NEAREST_MIPMAP_NEAREST
|
||||
case .linear: GL_LINEAR_MIPMAP_NEAREST
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var glLinearMip: Int32
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .point: GL_NEAREST_MIPMAP_LINEAR
|
||||
case .linear: GL_LINEAR_MIPMAP_LINEAR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WrapMode
|
||||
{
|
||||
fileprivate var gl: Int32
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .clamping: GL_CLAMP
|
||||
case .clampBorder: GL_CLAMP_TO_BORDER
|
||||
case .clampEdge: GL_CLAMP_TO_EDGE
|
||||
case .mirrored: GL_MIRRORED_REPEAT
|
||||
case .repeating: GL_REPEAT
|
||||
}
|
||||
}
|
||||
}
|
796
Sources/JolkEngine/Renderer/OpenGLState.swift
Normal file
@ -0,0 +1,796 @@
|
||||
import OpenGL
|
||||
|
||||
|
||||
struct OpenGLState
|
||||
{
|
||||
struct Capability: OptionSet
|
||||
{
|
||||
let rawValue: UInt32
|
||||
|
||||
static let alphaTest = Self(rawValue: 1 << 0)
|
||||
static let blend = Self(rawValue: 1 << 1)
|
||||
static let colourMaterial = Self(rawValue: 1 << 2)
|
||||
static let colourSum = Self(rawValue: 1 << 3)
|
||||
static let cullFace = Self(rawValue: 1 << 4)
|
||||
static let depthTest = Self(rawValue: 1 << 5)
|
||||
static let dither = Self(rawValue: 1 << 6)
|
||||
static let fog = Self(rawValue: 1 << 7)
|
||||
static let light0 = Self(rawValue: 1 << 8)
|
||||
static let light1 = Self(rawValue: 1 << 9)
|
||||
static let light2 = Self(rawValue: 1 << 10)
|
||||
static let light3 = Self(rawValue: 1 << 11)
|
||||
static let light4 = Self(rawValue: 1 << 12)
|
||||
static let light5 = Self(rawValue: 1 << 13)
|
||||
static let light6 = Self(rawValue: 1 << 14)
|
||||
static let light7 = Self(rawValue: 1 << 15)
|
||||
static let lighting = Self(rawValue: 1 << 16)
|
||||
//static let lineSmooth = Self(rawValue: 1 << 17)
|
||||
static let lineStipple = Self(rawValue: 1 << 18)
|
||||
static let multiSample = Self(rawValue: 1 << 19)
|
||||
//static let pointSmooth = Self(rawValue: 1 << 20)
|
||||
static let scissorTest = Self(rawValue: 1 << 21)
|
||||
static let stencilTest = Self(rawValue: 1 << 22)
|
||||
//static let texture1D = Self(rawValue: 1 << 23)
|
||||
static let texture2D = Self(rawValue: 1 << 24)
|
||||
static let clipPlane0 = Self(rawValue: 1 << 25)
|
||||
static let clipPlane1 = Self(rawValue: 1 << 26)
|
||||
static let clipPlane2 = Self(rawValue: 1 << 27)
|
||||
static let clipPlane3 = Self(rawValue: 1 << 28)
|
||||
static let clipPlane4 = Self(rawValue: 1 << 29)
|
||||
static let clipPlane5 = Self(rawValue: 1 << 30)
|
||||
//static let colourLogicOp = Self(rawValue: 1 << 31)
|
||||
|
||||
static let sampleCoverage = Self(rawValue: 1 << 17)
|
||||
static let alphaToOne = Self(rawValue: 1 << 20)
|
||||
static let rescaleNormal = Self(rawValue: 1 << 23)
|
||||
static let frameBufferSrgb = Self(rawValue: 1 << 31)
|
||||
|
||||
static let all: [Self] = [ .alphaTest, .blend, .colourMaterial, .colourSum, .cullFace, .depthTest, .dither,
|
||||
.fog, .light0, .light1, .light2, .light3, .light4, .light5, .light6, .light7, .lighting, /*.lineSmooth,*/
|
||||
.lineStipple, /*.pointSmooth,*/ .multiSample, .scissorTest, .stencilTest, /*.texture1D,*/ .texture2D,
|
||||
.clipPlane0, .clipPlane1, .clipPlane2, .clipPlane3, .clipPlane4, .clipPlane5, /*.colourLogicOp,*/
|
||||
|
||||
.sampleCoverage, .alphaToOne, .rescaleNormal, .frameBufferSrgb ]
|
||||
static let initial: Self = [ .dither, .multiSample ]
|
||||
|
||||
var glCap: GLenum
|
||||
{
|
||||
let capEnum = switch self
|
||||
{
|
||||
case .alphaTest: GL_ALPHA_TEST
|
||||
case .blend: GL_BLEND
|
||||
case .colourMaterial: GL_COLOR_MATERIAL
|
||||
case .colourSum: GL_COLOR_SUM
|
||||
case .cullFace: GL_CULL_FACE
|
||||
case .depthTest: GL_DEPTH_TEST
|
||||
case .dither: GL_DITHER
|
||||
case .fog: GL_FOG
|
||||
case .light0: GL_LIGHT0
|
||||
case .light1: GL_LIGHT1
|
||||
case .light2: GL_LIGHT2
|
||||
case .light3: GL_LIGHT3
|
||||
case .light4: GL_LIGHT4
|
||||
case .light5: GL_LIGHT5
|
||||
case .light6: GL_LIGHT6
|
||||
case .light7: GL_LIGHT7
|
||||
case .lighting: GL_LIGHTING
|
||||
//case .lineSmooth: GL_LINE_SMOOTH
|
||||
case .lineStipple: GL_LINE_STIPPLE
|
||||
case .multiSample: GL_MULTISAMPLE
|
||||
//case .pointSmooth: GL_POINT_SMOOTH
|
||||
case .scissorTest: GL_SCISSOR_TEST
|
||||
case .stencilTest: GL_STENCIL_TEST
|
||||
//case .texture1D: GL_TEXTURE_1D
|
||||
case .texture2D: GL_TEXTURE_2D
|
||||
case .clipPlane0: GL_CLIP_PLANE0
|
||||
case .clipPlane1: GL_CLIP_PLANE1
|
||||
case .clipPlane2: GL_CLIP_PLANE2
|
||||
case .clipPlane3: GL_CLIP_PLANE3
|
||||
case .clipPlane4: GL_CLIP_PLANE4
|
||||
case .clipPlane5: GL_CLIP_PLANE5
|
||||
//case .colourLogicOp: GL_COLOR_LOGIC_OP
|
||||
case .sampleCoverage: GL_SAMPLE_COVERAGE
|
||||
case .alphaToOne: GL_SAMPLE_ALPHA_TO_ONE
|
||||
case .rescaleNormal: GL_RESCALE_NORMAL
|
||||
case .frameBufferSrgb: GL_FRAMEBUFFER_SRGB
|
||||
default: fatalError()
|
||||
}
|
||||
return GLenum(capEnum)
|
||||
}
|
||||
}
|
||||
|
||||
private var _capabilities: Capability = .initial
|
||||
|
||||
private mutating func toggle(_ cap: Capability, _ en: Bool)
|
||||
{
|
||||
if en
|
||||
{
|
||||
if !_capabilities.contains(cap)
|
||||
{
|
||||
glEnable(cap.glCap)
|
||||
_capabilities.insert(cap)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if _capabilities.contains(cap)
|
||||
{
|
||||
glDisable(cap.glCap)
|
||||
_capabilities.remove(cap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func enable(_ caps: Capability)
|
||||
{
|
||||
if Capability.all.contains(caps)
|
||||
{
|
||||
toggle(caps, true)
|
||||
}
|
||||
else
|
||||
{
|
||||
for i in Capability.all
|
||||
{
|
||||
if i.intersection(caps) == i { toggle(i, true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func disable(_ caps: Capability)
|
||||
{
|
||||
if Capability.all.contains(caps)
|
||||
{
|
||||
toggle(caps, false)
|
||||
}
|
||||
else
|
||||
{
|
||||
for i in Capability.all
|
||||
{
|
||||
if i.intersection(caps) == i { toggle(i, false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ClientState: OptionSet
|
||||
{
|
||||
let rawValue: UInt32
|
||||
static let colour = Self(rawValue: 1 << 0)
|
||||
static let edgeFlag = Self(rawValue: 1 << 1)
|
||||
static let fogCoord = Self(rawValue: 1 << 2)
|
||||
static let index = Self(rawValue: 1 << 3)
|
||||
static let normal = Self(rawValue: 1 << 4)
|
||||
static let secondaryColour = Self(rawValue: 1 << 5)
|
||||
static let textureCoord = Self(rawValue: 1 << 6)
|
||||
static let vertex = Self(rawValue: 1 << 7)
|
||||
|
||||
static let all: [Self] = [ .colour, .edgeFlag, .fogCoord, .index, .normal, .secondaryColour, .textureCoord, .vertex ]
|
||||
static let initial: Self = []
|
||||
|
||||
var glCap: GLenum
|
||||
{
|
||||
let capEnum = switch self
|
||||
{
|
||||
case .colour: GL_COLOR_ARRAY
|
||||
case .edgeFlag: GL_EDGE_FLAG_ARRAY
|
||||
case .fogCoord: GL_FOG_COORD_ARRAY
|
||||
case .index: GL_INDEX_ARRAY
|
||||
case .normal: GL_NORMAL_ARRAY
|
||||
case .secondaryColour: GL_SECONDARY_COLOR_ARRAY
|
||||
case .textureCoord: GL_TEXTURE_COORD_ARRAY
|
||||
case .vertex: GL_VERTEX_ARRAY
|
||||
default: fatalError()
|
||||
}
|
||||
return GLenum(capEnum)
|
||||
}
|
||||
}
|
||||
|
||||
private var _clientState: ClientState = .initial
|
||||
|
||||
private mutating func toggleClientState(_ cap: ClientState, _ en: Bool)
|
||||
{
|
||||
if en
|
||||
{
|
||||
if !_clientState.contains(cap)
|
||||
{
|
||||
glEnableClientState(cap.glCap)
|
||||
_clientState.insert(cap)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if _clientState.contains(cap)
|
||||
{
|
||||
glDisableClientState(cap.glCap)
|
||||
_clientState.remove(cap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func enableClient(_ caps: ClientState)
|
||||
{
|
||||
if ClientState.all.contains(caps)
|
||||
{
|
||||
toggleClientState(caps, true)
|
||||
}
|
||||
else
|
||||
{
|
||||
for i in ClientState.all
|
||||
{
|
||||
if i.intersection(caps) == i { toggleClientState(i, true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func disableClient(_ caps: ClientState)
|
||||
{
|
||||
if ClientState.all.contains(caps)
|
||||
{
|
||||
toggleClientState(caps, false)
|
||||
}
|
||||
else
|
||||
{
|
||||
for i in ClientState.all
|
||||
{
|
||||
if i.intersection(caps) == i { toggleClientState(i, false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _activeTexture2D = 0
|
||||
|
||||
private mutating func setActiveTexture2D(_ active: Int)
|
||||
{
|
||||
if active != _activeTexture2D
|
||||
{
|
||||
let slots = [ GL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2, GL_TEXTURE3, GL_TEXTURE4, GL_TEXTURE5, GL_TEXTURE6,
|
||||
GL_TEXTURE7, GL_TEXTURE8, GL_TEXTURE9, GL_TEXTURE10, GL_TEXTURE11, GL_TEXTURE12, GL_TEXTURE13,
|
||||
GL_TEXTURE14, GL_TEXTURE15, GL_TEXTURE16, GL_TEXTURE17, GL_TEXTURE18, GL_TEXTURE19, GL_TEXTURE20,
|
||||
GL_TEXTURE21, GL_TEXTURE22, GL_TEXTURE23, GL_TEXTURE24, GL_TEXTURE25, GL_TEXTURE26, GL_TEXTURE27,
|
||||
GL_TEXTURE28, GL_TEXTURE29, GL_TEXTURE30, GL_TEXTURE31 ]
|
||||
|
||||
glActiveTexture(GLenum(slots[active]))
|
||||
_activeTexture2D = active
|
||||
}
|
||||
}
|
||||
|
||||
static let defaultTexture: UInt32 = 0
|
||||
private var _texture2D: GLuint = GLuint(defaultTexture)
|
||||
|
||||
mutating func bindTexture2D(active: Int, texture: UInt32)
|
||||
{
|
||||
if GLuint(texture) != _texture2D
|
||||
{
|
||||
setActiveTexture2D(active)
|
||||
glBindTexture(GLenum(GL_TEXTURE_2D), GLuint(texture))
|
||||
_texture2D = GLuint(texture)
|
||||
}
|
||||
}
|
||||
|
||||
enum TexureParameter { case magFilter, minFilter, wrapS, wrapT }
|
||||
|
||||
mutating func texture2DParameter(active: Int, param: TexureParameter, int value: Int32)
|
||||
{
|
||||
setActiveTexture2D(active)
|
||||
let pname = switch param
|
||||
{
|
||||
case .magFilter: GL_TEXTURE_MAG_FILTER
|
||||
case .minFilter: GL_TEXTURE_MIN_FILTER
|
||||
case .wrapS: GL_TEXTURE_WRAP_S
|
||||
case .wrapT: GL_TEXTURE_WRAP_T
|
||||
}
|
||||
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(pname), GLint(value))
|
||||
}
|
||||
|
||||
private var _clearDepth: GLclampd = 1
|
||||
|
||||
var clearDepth: Double
|
||||
{
|
||||
get { Double(_clearDepth) }
|
||||
set(newDepth)
|
||||
{
|
||||
if GLclampd(newDepth) != _clearDepth
|
||||
{
|
||||
glClearDepth(newDepth)
|
||||
_clearDepth = GLclampd(newDepth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _clearColour: Colour = .zero
|
||||
|
||||
mutating func clearColour(_ newColour: Colour)
|
||||
{
|
||||
if newColour != _clearColour
|
||||
{
|
||||
glClearColor(newColour.r, newColour.g, newColour.b, newColour.a)
|
||||
_clearColour = newColour
|
||||
}
|
||||
}
|
||||
|
||||
enum CullFace { case front, back, frontAndBack }
|
||||
private var _cullFace: CullFace = .back
|
||||
|
||||
var cullFace: CullFace
|
||||
{
|
||||
get { _cullFace }
|
||||
set(newMode)
|
||||
{
|
||||
if newMode != _cullFace
|
||||
{
|
||||
let modeEnum = switch newMode
|
||||
{
|
||||
case .front: GL_FRONT
|
||||
case .back: GL_BACK
|
||||
case .frontAndBack: GL_FRONT_AND_BACK
|
||||
}
|
||||
glCullFace(GLenum(modeEnum))
|
||||
_cullFace = newMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DepthFunc { case never, less, equal, lessOrEqual, greater, notEqual, greaterOrEqual, always }
|
||||
private var _depthFunc: DepthFunc = .less
|
||||
|
||||
var depthFunc: DepthFunc
|
||||
{
|
||||
get { _depthFunc }
|
||||
set(newFunc)
|
||||
{
|
||||
if newFunc != _depthFunc
|
||||
{
|
||||
let funcEnum = switch newFunc
|
||||
{
|
||||
case .never: GL_NEVER
|
||||
case .less: GL_LESS
|
||||
case .equal: GL_EQUAL
|
||||
case .lessOrEqual: GL_LEQUAL
|
||||
case .greater: GL_GREATER
|
||||
case .notEqual: GL_NOTEQUAL
|
||||
case .greaterOrEqual: GL_GEQUAL
|
||||
case .always: GL_ALWAYS
|
||||
}
|
||||
glDepthFunc(GLenum(funcEnum))
|
||||
_depthFunc = newFunc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FogMode { case linear, exp, exp2 }
|
||||
private var _fogMode: FogMode = .exp
|
||||
|
||||
var fogMode: FogMode
|
||||
{
|
||||
get { _fogMode }
|
||||
set(newMode)
|
||||
{
|
||||
if newMode != _fogMode
|
||||
{
|
||||
let modeEnum = switch newMode
|
||||
{
|
||||
case .linear: GL_LINEAR
|
||||
case .exp: GL_EXP
|
||||
case .exp2: GL_EXP2
|
||||
}
|
||||
glFogi(GLenum(GL_FOG_MODE), modeEnum)
|
||||
_fogMode = newMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _fogDensity: Float = 1
|
||||
|
||||
var fogDensity: Float
|
||||
{
|
||||
get { _fogDensity }
|
||||
set(newDensity)
|
||||
{
|
||||
if newDensity != _fogDensity
|
||||
{
|
||||
glFogf(GLenum(GL_FOG_DENSITY), newDensity)
|
||||
_fogDensity = newDensity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _fogStart: Float = 0
|
||||
|
||||
var fogStart: Float
|
||||
{
|
||||
get { _fogStart }
|
||||
set(newStart)
|
||||
{
|
||||
if newStart != _fogStart
|
||||
{
|
||||
glFogf(GLenum(GL_FOG_START), newStart)
|
||||
_fogStart = newStart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _fogEnd: Float = 1
|
||||
|
||||
var fogEnd: Float
|
||||
{
|
||||
get { _fogEnd }
|
||||
set(newEnd)
|
||||
{
|
||||
if newEnd != _fogEnd
|
||||
{
|
||||
glFogf(GLenum(GL_FOG_END), newEnd)
|
||||
_fogEnd = newEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _fogColour: Colour = .zero
|
||||
|
||||
var fogColour: Colour
|
||||
{
|
||||
get { _fogColour }
|
||||
set(newColour)
|
||||
{
|
||||
if newColour != _fogColour
|
||||
{
|
||||
withUnsafePointer(to: newColour)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
|
||||
{
|
||||
glFogfv(GLenum(GL_FOG_COLOR), $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FogCoordinateSource { case coordinate, fragmentDepth }
|
||||
private var _fogCoordinateSource: FogCoordinateSource = .fragmentDepth
|
||||
|
||||
var fogCoodinateSource: FogCoordinateSource
|
||||
{
|
||||
get { _fogCoordinateSource }
|
||||
set(newCoordinateSource)
|
||||
{
|
||||
if newCoordinateSource != _fogCoordinateSource
|
||||
{
|
||||
let coordinateSourceEnum = switch newCoordinateSource
|
||||
{
|
||||
case .coordinate: GL_FOG_COORD
|
||||
case .fragmentDepth: GL_FRAGMENT_DEPTH
|
||||
}
|
||||
glFogi(GLenum(GL_FOG_COORD_SRC), coordinateSourceEnum)
|
||||
_fogCoordinateSource = newCoordinateSource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Hint
|
||||
{
|
||||
case fog
|
||||
case generateMipmap
|
||||
case lineSmooth
|
||||
case perspectiveCorrection
|
||||
case pointSmooth
|
||||
case polygonSmooth
|
||||
case textureCompression
|
||||
}
|
||||
enum HintBehaviour { case fastest, nicest, dontCare }
|
||||
|
||||
func setHint(target: Hint, hint: HintBehaviour)
|
||||
{
|
||||
let target = switch target
|
||||
{
|
||||
case .fog: GL_FOG_HINT
|
||||
case .generateMipmap: GL_GENERATE_MIPMAP_HINT
|
||||
case .lineSmooth: GL_LINE_SMOOTH_HINT
|
||||
case .perspectiveCorrection: GL_PERSPECTIVE_CORRECTION_HINT
|
||||
case .pointSmooth: GL_POINT_SMOOTH_HINT
|
||||
case .polygonSmooth: GL_POLYGON_SMOOTH_HINT
|
||||
case .textureCompression: GL_TEXTURE_COMPRESSION_HINT
|
||||
}
|
||||
let mode = switch hint
|
||||
{
|
||||
case .fastest: GL_FASTEST
|
||||
case .nicest: GL_NICEST
|
||||
case .dontCare: GL_DONT_CARE
|
||||
}
|
||||
glHint(GLenum(target), GLenum(mode))
|
||||
}
|
||||
|
||||
struct LightAmbientProperty
|
||||
{
|
||||
private var _lightAmbient = [Colour](repeating: .black, count: 8)
|
||||
|
||||
subscript(index: Int) -> Colour
|
||||
{
|
||||
get { _lightAmbient[index] }
|
||||
set(newAmbient)
|
||||
{
|
||||
if newAmbient != _lightAmbient[index]
|
||||
{
|
||||
withUnsafePointer(to: newAmbient)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
|
||||
{
|
||||
// glLightModelfv(GLenum(GL_LIGHT_MODEL_AMBIENT), ambientColour)
|
||||
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_AMBIENT), $0)
|
||||
}
|
||||
}
|
||||
_lightAmbient[index] = newAmbient
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightAmbient = LightAmbientProperty()
|
||||
|
||||
struct LightDiffuseProperty
|
||||
{
|
||||
private var _lightDiffuse: [Colour] = [ .white, .black, .black, .black, .black, .black, .black, .black ]
|
||||
|
||||
subscript(index: Int) -> Colour
|
||||
{
|
||||
get { _lightDiffuse[index] }
|
||||
set(newDiffuse)
|
||||
{
|
||||
if newDiffuse != _lightDiffuse[index]
|
||||
{
|
||||
withUnsafePointer(to: newDiffuse)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
|
||||
{
|
||||
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_DIFFUSE), $0)
|
||||
}
|
||||
}
|
||||
_lightDiffuse[index] = newDiffuse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightDiffuse = LightDiffuseProperty()
|
||||
|
||||
struct LightSpecularProperty
|
||||
{
|
||||
private var _lightSpecular: [Colour] = [ .white, .black, .black, .black, .black, .black, .black, .black ]
|
||||
|
||||
subscript(index: Int) -> Colour
|
||||
{
|
||||
get { _lightSpecular[index] }
|
||||
set(newSpecular)
|
||||
{
|
||||
if newSpecular != _lightSpecular[index]
|
||||
{
|
||||
withUnsafePointer(to: newSpecular)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
|
||||
{
|
||||
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_SPECULAR), $0)
|
||||
}
|
||||
}
|
||||
_lightSpecular[index] = newSpecular
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightSpecular = LightSpecularProperty()
|
||||
|
||||
struct LightPositionProperty
|
||||
{
|
||||
private var _lightPosition = [Vec4f](repeating: .Z, count: 8)
|
||||
|
||||
subscript(index: Int) -> Vec4f
|
||||
{
|
||||
get { _lightPosition[index] }
|
||||
set(newPosition)
|
||||
{
|
||||
withUnsafePointer(to: newPosition)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
|
||||
{
|
||||
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_POSITION), $0)
|
||||
}
|
||||
}
|
||||
_lightPosition[index] = newPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightPosition = LightPositionProperty()
|
||||
|
||||
//private var _lightSpotDirection = [Vec4f](repeating: .zero, count: 8)
|
||||
//private var _lightSpotExponent = [Vec4f](repeating: .zero, count: 8)
|
||||
//private var _lightSpotCutoff = [Vec4f](repeating: .zero, count: 8)
|
||||
|
||||
struct LightConstantAttenuationProperty
|
||||
{
|
||||
private var _lightConstantAttenuation = [Float](repeating: 1, count: 8)
|
||||
|
||||
subscript(index: Int) -> Float
|
||||
{
|
||||
get { _lightConstantAttenuation[index] }
|
||||
set(newConstantAttenuation)
|
||||
{
|
||||
if newConstantAttenuation != _lightConstantAttenuation[index]
|
||||
{
|
||||
glLightf(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_CONSTANT_ATTENUATION), newConstantAttenuation)
|
||||
_lightConstantAttenuation[index] = newConstantAttenuation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightConstantAttenuation = LightConstantAttenuationProperty()
|
||||
|
||||
struct LightLinearAttenuationProperty
|
||||
{
|
||||
private var _lightLinearAttenuation = [Float](repeating: 0, count: 8)
|
||||
|
||||
subscript(index: Int) -> Float
|
||||
{
|
||||
get { _lightLinearAttenuation[index] }
|
||||
set(newLinearAttenuation)
|
||||
{
|
||||
if newLinearAttenuation != _lightLinearAttenuation[index]
|
||||
{
|
||||
glLightf(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_LINEAR_ATTENUATION), newLinearAttenuation)
|
||||
_lightLinearAttenuation[index] = newLinearAttenuation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightLinearAttenuation = LightLinearAttenuationProperty()
|
||||
|
||||
struct LightQuadraticAttenuation
|
||||
{
|
||||
private var _lightQuadraticAttenuation = [Float](repeating: 0, count: 8)
|
||||
|
||||
subscript(index: Int) -> Float
|
||||
{
|
||||
get { _lightQuadraticAttenuation[index] }
|
||||
set(newQuadraticAttenuation)
|
||||
{
|
||||
if newQuadraticAttenuation != _lightQuadraticAttenuation[index]
|
||||
{
|
||||
glLightf(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_QUADRATIC_ATTENUATION), newQuadraticAttenuation)
|
||||
_lightQuadraticAttenuation[index] = newQuadraticAttenuation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lightQuadraticAttenuation = LightQuadraticAttenuation()
|
||||
|
||||
var _lightModelAmbient = Colour(r: 0.2, g: 0.2, b: 0.2, a: 1)
|
||||
|
||||
var lightModelAmbient: Colour
|
||||
{
|
||||
get { _lightModelAmbient }
|
||||
set(newAmbient)
|
||||
{
|
||||
if newAmbient != _lightModelAmbient
|
||||
{
|
||||
|
||||
withUnsafePointer(to: newAmbient)
|
||||
{
|
||||
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
|
||||
{
|
||||
glLightModelfv(GLenum(GL_LIGHT_MODEL_AMBIENT), $0)
|
||||
}
|
||||
}
|
||||
_lightModelAmbient = newAmbient
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum LightModelColourControl { case single, separateSpecular }
|
||||
var _lightModelColourControl: LightModelColourControl = .single
|
||||
|
||||
var lightModelColourControl: LightModelColourControl
|
||||
{
|
||||
get { _lightModelColourControl }
|
||||
set(newControl)
|
||||
{
|
||||
if newControl != _lightModelColourControl
|
||||
{
|
||||
let param = switch newControl
|
||||
{
|
||||
case .single: GL_SINGLE_COLOR
|
||||
case .separateSpecular: GL_SEPARATE_SPECULAR_COLOR
|
||||
}
|
||||
glLightModelf(GLenum(GL_LIGHT_MODEL_COLOR_CONTROL), GLfloat(param))
|
||||
_lightModelColourControl = newControl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _lightModelLocalViewer = false
|
||||
|
||||
var lightModelLocalViewer: Bool
|
||||
{
|
||||
get { _lightModelLocalViewer }
|
||||
set(newMode)
|
||||
{
|
||||
if newMode != _lightModelLocalViewer
|
||||
{
|
||||
glLightModeli(GLenum(GL_LIGHT_MODEL_LOCAL_VIEWER), newMode ? 1 : 0)
|
||||
_lightModelLocalViewer = newMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _lightModeTwoSide = false
|
||||
|
||||
var lightModeTwoSide: Bool
|
||||
{
|
||||
get { _lightModeTwoSide }
|
||||
set(newMode)
|
||||
{
|
||||
if newMode != _lightModeTwoSide
|
||||
{
|
||||
glLightModeli(GLenum(GL_LIGHT_MODEL_TWO_SIDE), newMode ? 1 : 0)
|
||||
_lightModeTwoSide = newMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MatrixMode { case modelView, projection, texture, colour }
|
||||
private var _matrixMode: MatrixMode = .modelView
|
||||
|
||||
var matrixMode: MatrixMode
|
||||
{
|
||||
get { _matrixMode }
|
||||
set(newMode)
|
||||
{
|
||||
if newMode != _matrixMode
|
||||
{
|
||||
let modeEnum = switch newMode
|
||||
{
|
||||
case .modelView: GL_MODELVIEW
|
||||
case .projection: GL_PROJECTION
|
||||
case .texture: GL_TEXTURE
|
||||
case .colour: GL_COLOR
|
||||
}
|
||||
glMatrixMode(GLenum(modeEnum))
|
||||
_matrixMode = newMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static let defaultBuffer: UInt32 = 0
|
||||
|
||||
private var _arrayBuffer = GLuint(defaultBuffer)
|
||||
|
||||
var arrayBuffer: UInt32
|
||||
{
|
||||
get { UInt32(_arrayBuffer) }
|
||||
set(newBuf)
|
||||
{
|
||||
if newBuf != _arrayBuffer
|
||||
{
|
||||
_arrayBuffer = GLuint(newBuf)
|
||||
glBindBuffer(GLenum(GL_ARRAY_BUFFER), _arrayBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _elementArrayBuffer = GLuint(defaultBuffer)
|
||||
|
||||
var elementArrayBuffer: UInt32
|
||||
{
|
||||
get { UInt32(_elementArrayBuffer) }
|
||||
set(newBuf)
|
||||
{
|
||||
if newBuf != _elementArrayBuffer
|
||||
{
|
||||
_elementArrayBuffer = GLuint(newBuf)
|
||||
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), _elementArrayBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
134
Sources/JolkEngine/Renderer/Renderer.swift
Normal file
@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
import OpenGL.GL
|
||||
|
||||
|
||||
public protocol Renderer
|
||||
{
|
||||
func create(sdlWindow: OpaquePointer) throws
|
||||
func delete()
|
||||
|
||||
func newFrame()
|
||||
func resize(width: Int32, height: Int32)
|
||||
|
||||
var clearColour: Colour { get set }
|
||||
|
||||
func setVsync(mode: VSyncMode) throws
|
||||
|
||||
func createMesh(mesh: Mesh) throws -> RenderMesh
|
||||
func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D
|
||||
func createTexture(data: UnsafeRawPointer, width: Int, height: Int,
|
||||
filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D
|
||||
|
||||
func deleteMesh(_ mesh: RenderMesh)
|
||||
func deleteTexture(_ texture: RenderTexture2D)
|
||||
|
||||
func setProjection(matrix: Mat4f)
|
||||
func setView(matrix: Mat4f)
|
||||
|
||||
func setMaterial(_ mat: Material)
|
||||
|
||||
func draw(mesh: RenderMesh, model: Mat4f, environment: Environment)
|
||||
func draw(mesh: RenderMesh, environment: Environment)
|
||||
|
||||
func drawGizmos(lines: [Line])
|
||||
}
|
||||
|
||||
public enum RendererError: Error
|
||||
{
|
||||
case sdlError(message: String)
|
||||
}
|
||||
|
||||
public enum VSyncMode
|
||||
{
|
||||
case off
|
||||
case on
|
||||
case adaptive
|
||||
}
|
||||
|
||||
public enum FilterMode
|
||||
{
|
||||
case point
|
||||
case linear
|
||||
}
|
||||
|
||||
public enum MipMode
|
||||
{
|
||||
case off
|
||||
case nearest
|
||||
case linear
|
||||
}
|
||||
|
||||
public enum WrapMode
|
||||
{
|
||||
case clamping
|
||||
case clampBorder
|
||||
case clampEdge
|
||||
case mirrored
|
||||
case repeating
|
||||
}
|
||||
|
||||
public protocol RendererResource
|
||||
{
|
||||
associatedtype T: Resource
|
||||
|
||||
static var empty: Self { get }
|
||||
|
||||
var isValid: Bool { get }
|
||||
}
|
||||
|
||||
public struct RenderMesh: RendererResource
|
||||
{
|
||||
public typealias T = Mesh
|
||||
|
||||
public static var empty: RenderMesh { .init() }
|
||||
|
||||
public var isValid: Bool { _valid }
|
||||
|
||||
private let _valid: Bool;
|
||||
let vboHnd: Int, iboHnd: Int
|
||||
let subMeshes: [Mesh.SubMesh]
|
||||
|
||||
private init()
|
||||
{
|
||||
self._valid = false
|
||||
self.vboHnd = 0
|
||||
self.iboHnd = 0
|
||||
self.subMeshes = []
|
||||
}
|
||||
|
||||
init(vbo: Int, ibo: Int, subMeshes: [Mesh.SubMesh])
|
||||
{
|
||||
self._valid = true
|
||||
self.vboHnd = vbo
|
||||
self.iboHnd = ibo
|
||||
self.subMeshes = subMeshes
|
||||
}
|
||||
}
|
||||
|
||||
public struct RenderTexture2D: RendererResource
|
||||
{
|
||||
public typealias T = Texture2D
|
||||
|
||||
public static var empty: Self { .init(0) }
|
||||
|
||||
public var isValid: Bool { id != 0 }
|
||||
|
||||
let id: UInt32
|
||||
|
||||
init(_ id: UInt32)
|
||||
{
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
public struct Line
|
||||
{
|
||||
let from: Vec3f, to: Vec3f, colour: Colour
|
||||
|
||||
public init(from: Vec3f, to: Vec3f, colour: Colour)
|
||||
{
|
||||
self.from = from
|
||||
self.to = to
|
||||
self.colour = colour
|
||||
}
|
||||
}
|
31
Sources/JolkEngine/Renderer/Texture2D.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct Texture2D: Resource
|
||||
{
|
||||
public let id: RenderTexture2D
|
||||
public let width: Int, height: Int
|
||||
}
|
||||
|
||||
extension Texture2D
|
||||
{
|
||||
public static let empty = Self(id: .empty, width: 0, height: 0)
|
||||
}
|
||||
|
||||
|
||||
public struct Texture2DParameters: ContentLoaderParametersProtocol
|
||||
{
|
||||
public typealias T = Texture2D
|
||||
|
||||
var minFilter: FilterMode, magFilter: FilterMode
|
||||
var wrapMode: WrapMode
|
||||
var mipMode: MipMode
|
||||
|
||||
public init(minFilter: FilterMode = .linear, magFilter: FilterMode = .linear, wrapMode: WrapMode = .repeating, mipMode: MipMode = .off)
|
||||
{
|
||||
self.minFilter = minFilter
|
||||
self.magFilter = magFilter
|
||||
self.wrapMode = wrapMode
|
||||
self.mipMode = mipMode
|
||||
}
|
||||
}
|
20
Sources/JolkEngine/Util/FileHandle.swift
Normal file
@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
extension FileHandle
|
||||
{
|
||||
// FixedWidthInteger or BinaryFloatingPoint
|
||||
func read<T: FixedWidthInteger>(as: T.Type) throws -> T
|
||||
{
|
||||
let size = MemoryLayout<T>.size
|
||||
guard let data = try self.read(upToCount: size)
|
||||
else { throw NSError(domain: "FileHandle.read", code: NSFileReadUnknownError, userInfo: [
|
||||
NSLocalizedFailureReasonErrorKey: "Read error",
|
||||
NSLocalizedDescriptionKey: "Read error"]) }
|
||||
guard data.count == size
|
||||
else { throw NSError(domain: "FileHandle.read", code: NSFileReadUnknownError, userInfo: [
|
||||
NSLocalizedFailureReasonErrorKey: "Value underread",
|
||||
NSLocalizedDescriptionKey: "Value underread"]) }
|
||||
return data.withUnsafeBytes { p -> T in p.load(as: T.self) }
|
||||
}
|
||||
}
|
52
Sources/JolkEngine/Util/TextFile.swift
Normal file
@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
class TextFile
|
||||
{
|
||||
private var _hnd: UnsafeMutablePointer<FILE>
|
||||
private let _maxLength: Int
|
||||
|
||||
init(fileURL: URL, maxLineLength: Int = 1024) throws
|
||||
{
|
||||
guard let f = fopen(fileURL.path, "r")
|
||||
else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) }
|
||||
|
||||
_hnd = f
|
||||
_maxLength = maxLineLength
|
||||
}
|
||||
|
||||
deinit
|
||||
{
|
||||
fclose(_hnd)
|
||||
}
|
||||
|
||||
struct LinesSequence: Sequence, IteratorProtocol
|
||||
{
|
||||
typealias Element = String
|
||||
|
||||
private var _hnd: UnsafeMutablePointer<FILE>
|
||||
private var _buffer: [CChar]
|
||||
|
||||
fileprivate init(_ handle: UnsafeMutablePointer<FILE>, _ maxLength: Int)
|
||||
{
|
||||
_hnd = handle
|
||||
_buffer = [CChar](repeating: 0, count: maxLength)
|
||||
}
|
||||
|
||||
mutating func next() -> String?
|
||||
{
|
||||
guard fgets(&_buffer, Int32(_buffer.count), _hnd) != nil
|
||||
else
|
||||
{
|
||||
if feof(_hnd) != 0 { return nil }
|
||||
else { return nil }
|
||||
}
|
||||
let length = strcspn(_buffer, "\r\n")
|
||||
_buffer[length] = "\0".utf8CString[0]
|
||||
//if let pos = strchr(_buffer, Int32("\n".utf8CString[0])) { pos[0] = "\0".utf8CString[0] }
|
||||
return String(cString: _buffer)
|
||||
}
|
||||
}
|
||||
|
||||
public var lines: LinesSequence { LinesSequence(_hnd, _maxLength) }
|
||||
}
|
248
Sources/JolkEngine/Util/UBJsonReader.swift
Normal file
@ -0,0 +1,248 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
struct UBJsonReader
|
||||
{
|
||||
private var file: FileHandle
|
||||
|
||||
init(file: FileHandle)
|
||||
{
|
||||
self.file = file
|
||||
}
|
||||
|
||||
func read() throws -> UBJsonToken
|
||||
{
|
||||
guard try readCharacter() == "{"
|
||||
else { throw UBReaderError.badFormat("Stream does not start with an object") }
|
||||
return try readObject()
|
||||
}
|
||||
|
||||
private func parse(type: Character) throws -> UBJsonToken
|
||||
{
|
||||
let oldFormat = true
|
||||
return switch type
|
||||
{
|
||||
case "{": try readObject()
|
||||
case "[": try readArray()
|
||||
case "Z": .null
|
||||
case "N": .noop
|
||||
case "T": .bool(true)
|
||||
case "F": .bool(false)
|
||||
case "i": oldFormat
|
||||
? .int16(try file.read(as: Int16.self).bigEndian)
|
||||
: .int8(try file.read(as: Int8.self))
|
||||
case "U": .uint8(try file.read(as: UInt8.self))
|
||||
case "I": oldFormat
|
||||
? .int32(try file.read(as: Int32.self).bigEndian)
|
||||
: .int16(try file.read(as: Int16.self).bigEndian)
|
||||
case "l": .int32(try file.read(as: Int32.self).bigEndian)
|
||||
case "L": .int64(try file.read(as: Int64.self).bigEndian)
|
||||
case "d": .float32(try readBeFloatPiss())
|
||||
case "D": .float64(try readBeFloat())
|
||||
case "H": throw UBReaderError.badFormat("High-precision numbers are unsupported")
|
||||
case "C": .char(try readCharacter())
|
||||
case "S": .string(try readString())
|
||||
default: throw UBReaderError.badToken("Unexpected token \"\(type)\"")
|
||||
}
|
||||
}
|
||||
|
||||
private func readObject() throws -> UBJsonToken
|
||||
{
|
||||
var items = Dictionary<String, UBJsonToken>()
|
||||
while true
|
||||
{
|
||||
let type = try readCharacter()
|
||||
var name: String
|
||||
switch type
|
||||
{
|
||||
case "S", "i":
|
||||
name = try readString(type)
|
||||
case "}":
|
||||
return .object(items)
|
||||
default:
|
||||
throw UBReaderError.badToken("Unexpected token while reading object field key")
|
||||
}
|
||||
if items.keys.contains(name) { throw UBReaderError.badFormat("Object contains overlapping keys") }
|
||||
items[name] = try parse(type: try readCharacter())
|
||||
}
|
||||
}
|
||||
|
||||
private func readArray() throws -> UBJsonToken
|
||||
{
|
||||
var array = [UBJsonToken]()
|
||||
while true
|
||||
{
|
||||
let type = try readCharacter()
|
||||
switch type
|
||||
{
|
||||
case "]":
|
||||
return .array(array)
|
||||
default:
|
||||
array.append(try parse(type: type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readBeFloatPiss() throws -> Float
|
||||
{
|
||||
guard var bytes = try? file.read(upToCount: 4)
|
||||
else { throw UBReaderError.readError("Read failure while reading float data") }
|
||||
return Float(bitPattern: bytes.withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian)
|
||||
}
|
||||
|
||||
private func readBeFloat<T: BinaryFloatingPoint, U: UnsignedInteger>() throws -> T where T.RawSignificand == U
|
||||
{
|
||||
guard var bytes = try? file.read(upToCount: MemoryLayout<U>.size)
|
||||
else { throw UBReaderError.readError("Read failure while reading float data") }
|
||||
bytes.reverse()
|
||||
return T(bytes.withUnsafeBytes { $0.load(as: U.self) })
|
||||
}
|
||||
|
||||
private func readCharacter() throws -> Character
|
||||
{
|
||||
guard let raw = try? file.read(as: UInt8.self),
|
||||
let uni = UnicodeScalar(Int(raw))
|
||||
else { throw UBReaderError.readError("Read failure while reading character") }
|
||||
return Character(uni)
|
||||
}
|
||||
|
||||
private func readString(_ optType: Character? = nil) throws -> String
|
||||
{
|
||||
let type = optType == nil ? try readCharacter() : optType!
|
||||
var length: Int
|
||||
switch type
|
||||
{
|
||||
case "S":
|
||||
guard try readCharacter() == "i"
|
||||
else { throw UBReaderError.badToken("Malformed string") }
|
||||
fallthrough
|
||||
case "i":
|
||||
length = Int(try file.read(as: Int8.self))
|
||||
case "s":
|
||||
length = Int(try file.read(as: UInt8.self))
|
||||
default: throw UBReaderError.badToken("Unexpected token while reading string")
|
||||
}
|
||||
if length < 0 { throw UBReaderError.badToken("Negative string length") }
|
||||
if length == 0 { return "" }
|
||||
guard let data = try file.read(upToCount: length)
|
||||
else { throw UBReaderError.readError("Error reading string") }
|
||||
return String(decoding: data, as: UTF8.self)
|
||||
}
|
||||
}
|
||||
|
||||
enum UBReaderError: Error
|
||||
{
|
||||
case badFormat(_ message: String)
|
||||
case badToken(_ message: String)
|
||||
case readError(_ message: String)
|
||||
case valueError(_ message: String)
|
||||
}
|
||||
|
||||
enum UBJsonToken
|
||||
{
|
||||
case object(_ fields: Dictionary<String, UBJsonToken>)
|
||||
case array(_ items: [UBJsonToken])
|
||||
case null
|
||||
case noop
|
||||
case bool(_ value: Bool)
|
||||
case int8(_ value: Int8)
|
||||
case uint8(_ value: UInt8)
|
||||
case int16(_ value: Int16)
|
||||
case int32(_ value: Int32)
|
||||
case int64(_ value: Int64)
|
||||
case highPrecision
|
||||
case float32(_ value: Float32)
|
||||
case float64(_ value: Float64)
|
||||
case char(_ value: Character)
|
||||
case string(_ value: String)
|
||||
}
|
||||
|
||||
extension UBJsonToken
|
||||
{
|
||||
var array: [UBJsonToken]
|
||||
{
|
||||
get throws
|
||||
{
|
||||
if case .array(let items) = self { return items }
|
||||
throw UBReaderError.valueError("Not an array")
|
||||
}
|
||||
}
|
||||
|
||||
func getArray(key: String) throws -> [UBJsonToken]
|
||||
{
|
||||
if case .object(let fields) = self
|
||||
{
|
||||
guard let child = fields[key]
|
||||
else { throw UBReaderError.valueError("Field \"\(key)\" not found") }
|
||||
return try child.array
|
||||
}
|
||||
throw UBReaderError.valueError("Not an object")
|
||||
}
|
||||
|
||||
var int16: Int16
|
||||
{
|
||||
get throws
|
||||
{
|
||||
if case .int16(let value) = self { return value }
|
||||
throw UBReaderError.valueError("Not an int16")
|
||||
}
|
||||
}
|
||||
|
||||
func getInt16Array(key: String) throws -> [Int16]
|
||||
{
|
||||
try getArray(key: key).map(
|
||||
{ i in
|
||||
if case .int8(let value) = i { return Int16(value) }
|
||||
else if case .uint8(let value) = i { return Int16(value) }
|
||||
else if case .int16(let value) = i { return value }
|
||||
else if case .int32(let value) = i
|
||||
{
|
||||
if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") }
|
||||
return Int16(truncatingIfNeeded: value)
|
||||
}
|
||||
else if case .int64(let value) = i
|
||||
{
|
||||
if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") }
|
||||
return Int16(truncatingIfNeeded: value)
|
||||
}
|
||||
else { throw UBReaderError.valueError("Can't read array as int16s") }
|
||||
})
|
||||
}
|
||||
|
||||
var float: Float
|
||||
{
|
||||
get throws
|
||||
{
|
||||
if case .float32(let value) = self { return value }
|
||||
throw UBReaderError.valueError("Not a float32")
|
||||
}
|
||||
}
|
||||
|
||||
func getFloatArray(key: String) throws -> [Float]
|
||||
{
|
||||
try getArray(key: key).map({ try $0.float })
|
||||
}
|
||||
|
||||
var string: String
|
||||
{
|
||||
get throws
|
||||
{
|
||||
if case .string(let value) = self { return value }
|
||||
throw UBReaderError.valueError("Not a string")
|
||||
}
|
||||
}
|
||||
|
||||
func getString(key: String, default defStr: String? = nil) throws -> String
|
||||
{
|
||||
if case .object(let fields) = self
|
||||
{
|
||||
guard let child = fields[key] else
|
||||
{
|
||||
if defStr == nil { throw UBReaderError.valueError("Field \"\(key)\" not found") }
|
||||
return defStr!
|
||||
}
|
||||
return try child.string
|
||||
}
|
||||
throw UBReaderError.valueError("Not an object")
|
||||
}
|
||||
}
|
276
Sources/Test/CavesOfJolk.swift
Normal file
@ -0,0 +1,276 @@
|
||||
import Foundation
|
||||
import simd
|
||||
import JolkEngine
|
||||
|
||||
|
||||
@main struct Program
|
||||
{
|
||||
static func main()
|
||||
{
|
||||
Application(
|
||||
application: CavesOfJolk(),
|
||||
config: ApplicationConfiguration(
|
||||
resizable: true,
|
||||
vSync: .on,
|
||||
windowWidth: 1280,
|
||||
windowHeight: 720,
|
||||
bundle: Bundle.module)
|
||||
).run()
|
||||
}
|
||||
}
|
||||
|
||||
class CavesOfJolk: ApplicationImplementation
|
||||
{
|
||||
private var aspect: Float = 0
|
||||
private var drawEdges = false
|
||||
|
||||
var env = Environment()
|
||||
var worldMesh = RenderMesh.empty, cube = RenderMesh.empty, suzanne = RenderMesh.empty, toybox = RenderMesh.empty
|
||||
var texture = Texture2D.empty, jolkTex = Texture2D.empty, suzanneDiffuse = Texture2D.empty
|
||||
|
||||
private lazy var world = Collision()
|
||||
private lazy var colin = Colin()
|
||||
private lazy var jolkCube = JolkCube(position: .init(0, 1, -4))
|
||||
private var lightTheta: Float = 0
|
||||
|
||||
private var frameCount = 0
|
||||
private var frameTimer: Float = 1
|
||||
|
||||
func create(render: inout Renderer)
|
||||
{
|
||||
render.clearColour = XnaColour.CornflowerBlue
|
||||
|
||||
env.setFog(
|
||||
colour: XnaColour.CornflowerBlue,
|
||||
//mode: .depth, falloff: .range(type: .linear, start: 4, end: 38))
|
||||
//mode: .depth, falloff: .range(type: .linear, start: 1.25, end: 24.5))
|
||||
//mode: .distance, falloff: .factor(type: .exp2, density: 0.1))
|
||||
mode: .depth, falloff: .factor(type: .exp2, density: 0.0222))
|
||||
|
||||
env.addAmbientLight(colour: XnaColour.DarkSlateGray.lighten(by: -0.666))
|
||||
env.addDirectionalLight(
|
||||
direction: -Vec3f(1, -1, -1).normalised,
|
||||
colour: XnaColour.BlanchedAlmond
|
||||
.lighten(by: -0.02)
|
||||
.mix(with: XnaColour.LightSlateGray, 0.5)
|
||||
.mix(with: XnaColour.White, 0.125))
|
||||
env.addPointLight(
|
||||
position: Vec3f(3, 0.33, -5),
|
||||
colour: XnaColour.Green.mix(with: XnaColour.Gray, 0.5),
|
||||
intensity: 2)
|
||||
env.addPointLight(
|
||||
position: Vec3f(5.5, 0.33, -6),
|
||||
colour: XnaColour.Red.mix(with: XnaColour.Gray, 0.5),
|
||||
intensity: 2)
|
||||
env.addPointLight(
|
||||
position: Vec3f(4, 0.33, -7),
|
||||
colour: XnaColour.Blue.mix(with: XnaColour.Gray, 0.5),
|
||||
intensity: 2)
|
||||
}
|
||||
|
||||
func loadContent(content: inout ContentManager) throws
|
||||
{
|
||||
try loadWorld(&content)
|
||||
cube = try content.create(mesh: .init(
|
||||
vertices: [
|
||||
.init(position: Vec3f(-1, -1, 1), normal: .forward, texCoord: Vec2f(0, 0)),
|
||||
.init(position: Vec3f( 1, -1, 1), normal: .forward, texCoord: Vec2f(1, 0)),
|
||||
.init(position: Vec3f(-1, 1, 1), normal: .forward, texCoord: Vec2f(0, 1)),
|
||||
.init(position: Vec3f( 1, 1, 1), normal: .forward, texCoord: Vec2f(1, 1)),
|
||||
.init(position: Vec3f( 1, -1, 1), normal: .right, texCoord: Vec2f(0, 0)),
|
||||
.init(position: Vec3f( 1, -1, -1), normal: .right, texCoord: Vec2f(1, 0)),
|
||||
.init(position: Vec3f( 1, 1, 1), normal: .right, texCoord: Vec2f(0, 1)),
|
||||
.init(position: Vec3f( 1, 1, -1), normal: .right, texCoord: Vec2f(1, 1)),
|
||||
.init(position: Vec3f( 1, -1, -1), normal: .back, texCoord: Vec2f(0, 0)),
|
||||
.init(position: Vec3f(-1, -1, -1), normal: .back, texCoord: Vec2f(1, 0)),
|
||||
.init(position: Vec3f( 1, 1, -1), normal: .back, texCoord: Vec2f(0, 1)),
|
||||
.init(position: Vec3f(-1, 1, -1), normal: .back, texCoord: Vec2f(1, 1)),
|
||||
.init(position: Vec3f(-1, -1, -1), normal: .left, texCoord: Vec2f(0, 0)),
|
||||
.init(position: Vec3f(-1, -1, 1), normal: .left, texCoord: Vec2f(1, 0)),
|
||||
.init(position: Vec3f(-1, 1, -1), normal: .left, texCoord: Vec2f(0, 1)),
|
||||
.init(position: Vec3f(-1, 1, 1), normal: .left, texCoord: Vec2f(1, 1)),
|
||||
.init(position: Vec3f(-1, -1, -1), normal: .down, texCoord: Vec2f(0, 0)),
|
||||
.init(position: Vec3f( 1, -1, -1), normal: .down, texCoord: Vec2f(1, 0)),
|
||||
.init(position: Vec3f(-1, -1, 1), normal: .down, texCoord: Vec2f(0, 1)),
|
||||
.init(position: Vec3f( 1, -1, 1), normal: .down, texCoord: Vec2f(1, 1)),
|
||||
.init(position: Vec3f(-1, 1, 1), normal: .up, texCoord: Vec2f(0, 0)),
|
||||
.init(position: Vec3f( 1, 1, 1), normal: .up, texCoord: Vec2f(1, 0)),
|
||||
.init(position: Vec3f(-1, 1, -1), normal: .up, texCoord: Vec2f(0, 1)),
|
||||
.init(position: Vec3f( 1, 1, -1), normal: .up, texCoord: Vec2f(1, 1))
|
||||
],
|
||||
indices: [
|
||||
0, 1, 2, 2, 1, 3,
|
||||
4, 5, 6, 6, 5, 7,
|
||||
8, 9, 10, 10, 9, 11,
|
||||
12, 13, 14, 14, 13, 15,
|
||||
16, 17, 18, 18, 17, 19,
|
||||
20, 21, 22, 22, 21, 23 ]))
|
||||
suzanne = try content.load("suzanne.g3db")
|
||||
toybox = try content.load("toybox.g3db")
|
||||
|
||||
|
||||
let linearMipped = Texture2DParameters(
|
||||
minFilter: .linear, magFilter: .linear,
|
||||
wrapMode: .repeating,
|
||||
mipMode: .linear)
|
||||
let linearClamp = Texture2DParameters(
|
||||
minFilter: .linear, magFilter: .linear,
|
||||
wrapMode: .clampEdge)
|
||||
|
||||
texture = try content.load("cobblestone.png", params: linearMipped)
|
||||
jolkTex = try content.load("jolkmeup.jpg", params: linearClamp)
|
||||
suzanneDiffuse = try content.load("suzanne_diffuse.png", params: linearMipped)
|
||||
}
|
||||
|
||||
func resize(width: Int, height: Int)
|
||||
{
|
||||
aspect = Float(width) / Float(height)
|
||||
print(aspect)
|
||||
}
|
||||
|
||||
func update(deltaTime: Float)
|
||||
{
|
||||
colin.update(deltaTime: deltaTime, world: world)
|
||||
jolkCube.update(deltaTime: deltaTime, world: world)
|
||||
|
||||
lightTheta += deltaTime
|
||||
|
||||
frameCount += 1
|
||||
frameTimer -= deltaTime
|
||||
if frameTimer <= 0
|
||||
{
|
||||
print("FPS: \(frameCount)")
|
||||
frameCount = 0
|
||||
frameTimer += 1.0
|
||||
}
|
||||
}
|
||||
|
||||
private func loadWorld(_ content: inout ContentManager) throws
|
||||
{
|
||||
//let world: Mesh = try content.load("World.obj")
|
||||
let obj = try ObjReader.read(url: try content.getResource("World.obj"))
|
||||
let mesh: Mesh = try ObjLoader.read(model: obj)
|
||||
worldMesh = try content.create(mesh: mesh)
|
||||
|
||||
if let collision = obj.objects["Collision3D"]
|
||||
{
|
||||
world.build(obj: obj, collision: collision)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if let collision = world.subMeshes["Collision"]
|
||||
{
|
||||
edges.reserveCapacity(collision.length / 6)
|
||||
let get2dVertex =
|
||||
{ i in
|
||||
let vertex = world.vertices[Int(world.indices[i])]
|
||||
let position = Vec2f(vertex.position.x, vertex.position.z)
|
||||
let normal = Vec2f(vertex.normal.x, vertex.normal.z)
|
||||
return (position, normal)
|
||||
}
|
||||
for i in stride(from: collision.start, to: collision.start + collision.length, by: 3)
|
||||
{
|
||||
let (v0p, v0n) = get2dVertex(i)
|
||||
let (v1p, v1n) = get2dVertex(i + 1)
|
||||
let (v2p, v2n) = get2dVertex(i + 2)
|
||||
|
||||
let (p0, p1) = if v0p == v2p || v1p == v2p { (v0p, v1p) }
|
||||
else if v0p == v1p { (v0p, v2p) }
|
||||
else { throw NSError() }
|
||||
|
||||
let edge = Edge(p: p0.lerp(p1, 0.5), n: (v0n + v1n + v2n).normalised, w: (p0 - p1).len)
|
||||
if !edges.contains(where: { edge.p == $0.p && edge.w == $0.w })
|
||||
{
|
||||
edges.append(edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func draw(render: inout Renderer, deltaTime: Float)
|
||||
{
|
||||
// Setup camera
|
||||
render.setProjection(matrix:
|
||||
.perspective(fovY: Float.rad(fromDeg: colin.fov), aspect: aspect, zNear: 0.1, zFar: 100))
|
||||
render.setView(matrix: colin.transform)
|
||||
|
||||
// Update point lights
|
||||
var theta = Vec3f(repeating: lightTheta) * Vec3f(0.12, 0.011, 0.056) * 6
|
||||
var i = env.lights.startIndex
|
||||
while i != env.lights.endIndex
|
||||
{
|
||||
if case .point(let colour, _, _) = env.lights[i]
|
||||
{
|
||||
env.lights[i] = .point(
|
||||
colour: colour,
|
||||
position: Vec3f(
|
||||
4 + 6 * cos(theta[0]),
|
||||
0.33 + 0.33 * sin(theta[1]),
|
||||
-6 + 3 * sin(theta[0] * 2)),
|
||||
intensity: 3.1 + 1.53 * cos(theta[2]))
|
||||
|
||||
let spacing = 0.5 * Float.pi * (2 / 3.0)
|
||||
theta += spacing * Vec3f(1, 0.98, 0.5566)
|
||||
}
|
||||
i = env.lights.index(after: i)
|
||||
}
|
||||
|
||||
// Draw world
|
||||
render.setMaterial(Material(
|
||||
specular: XnaColour.BlanchedAlmond.mix(with: .black, 0.12),
|
||||
texture: texture.id))
|
||||
render.draw(mesh: worldMesh, environment: env)
|
||||
|
||||
// Draw jolked up shit
|
||||
render.setMaterial(Material(
|
||||
specular: XnaColour.Gray,
|
||||
gloss: 20.0,
|
||||
texture: jolkTex.id))
|
||||
render.draw(mesh: cube, model: jolkCube.transform, environment: env)
|
||||
|
||||
render.setMaterial(Material(texture: suzanneDiffuse.id))
|
||||
render.draw(mesh: suzanne, model: .translate(.up + Vec3f(3.0, 0.0, -3.5) * 2.5), environment: env)
|
||||
|
||||
render.draw(mesh: toybox,
|
||||
model: .translate(Vec3f(6.0, 0.667, -3.5) * Vec3f(2.5, 1, 2.5))
|
||||
* .rotate(y: lightTheta * 0.5) * .scale(scalar: 0.25), environment: env)
|
||||
|
||||
if Input.instance.keyPressed(.c) { drawEdges = !drawEdges }
|
||||
if drawEdges
|
||||
{
|
||||
/*
|
||||
var lines = [Line](
|
||||
repeating: .init(from: .init(), to: .init(), colour: .zero),
|
||||
count: edges.count * 3)
|
||||
var i: Int = 0
|
||||
for edge in edges
|
||||
{
|
||||
let tangent = Vec2f(edge.n.y, -edge.n.x)
|
||||
let a = edge.p - tangent * edge.w * 0.5
|
||||
let b = edge.p + tangent * edge.w * 0.5
|
||||
|
||||
lines[i] = Line(
|
||||
from: Vec3f(a.x, 0, a.y),
|
||||
to: Vec3f(b.x, 0, b.y),
|
||||
colour: Colour(r: 0.1, g: 0.9, b: 0.1))
|
||||
lines[i + 1] = Line(
|
||||
from: Vec3f(edge.p.x, 0, edge.p.y),
|
||||
to: Vec3f(edge.p.x + edge.n.x * 0.25, 0, edge.p.y + edge.n.y * 0.25),
|
||||
colour: Colour(r: 0.9, g: 0.1, b: 0.1))
|
||||
|
||||
let deltaPos = Vec2f(colin.position.x, colin.position.z) - edge.p
|
||||
let something = tangent * deltaPos.cross(edge.n)
|
||||
lines[i + 2] = Line(
|
||||
from: Vec3f(edge.p.x + something.x, 0.0, edge.p.y + something.y),
|
||||
to: Vec3f(edge.p.x + something.x + edge.n.x * 0.5, 0.0, edge.p.y + something.y + edge.n.y * 0.5),
|
||||
colour: XnaColour.Azure)
|
||||
i += 3
|
||||
}
|
||||
render.drawGizmos(lines: lines)
|
||||
*/
|
||||
|
||||
world.draw(render, position: colin.position)
|
||||
}
|
||||
}
|
||||
}
|
497
Sources/Test/Collision.swift
Normal file
@ -0,0 +1,497 @@
|
||||
import Foundation
|
||||
import JolkEngine
|
||||
|
||||
|
||||
class Collision
|
||||
{
|
||||
struct Edge { let p: Vec2f, n: Vec2f, w: Float }
|
||||
|
||||
enum Edge3D
|
||||
{
|
||||
case triangle(n: Vec3f, p: Vec3f, v: (Vec3f, Vec3f, Vec3f))
|
||||
case aabbFloor(n: Vec3f, p: Vec3f, w: Float, d: Float)
|
||||
case quad(p: (Vec3f, Vec3f, Vec3f, Vec3f), w: Winding)
|
||||
}
|
||||
var edge3d = [Edge3D]()
|
||||
|
||||
/*
|
||||
0,-2 1,-2
|
||||
*--*
|
||||
| |
|
||||
0,-1 * * 1,-1
|
||||
| |
|
||||
*--*
|
||||
0, 0 1, 0
|
||||
*/
|
||||
|
||||
|
||||
init()
|
||||
{
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(0.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, -1.0),
|
||||
Vec3f(1.0, 0.0, -1.0),
|
||||
Vec3f(1.0, 0.0, 0.0),
|
||||
]))
|
||||
|
||||
|
||||
// CW
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(0.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, -1.0),
|
||||
Vec3f(0.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -1.0),
|
||||
Vec3f(1.0, 0.0, 0.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(0.0, 0.0, -1.0),
|
||||
Vec3f(0.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -1.0),
|
||||
Vec3f(1.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, 0.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(0.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -1.0),
|
||||
Vec3f(1.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, -1.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(1.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -1.0),
|
||||
Vec3f(1.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, -1.0),
|
||||
Vec3f(0.0, 0.0, -2.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(1.0, 0.0, -1.0),
|
||||
Vec3f(1.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, -1.0),
|
||||
Vec3f(0.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -2.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(1.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, 0.0),
|
||||
Vec3f(0.0, 0.0, -1.0),
|
||||
Vec3f(0.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -2.0),
|
||||
Vec3f(1.0, 0.0, -1.0),
|
||||
]))
|
||||
|
||||
// CCW
|
||||
assert(Self.isRectangle([
|
||||
Vec3f( 0.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, -1.0),
|
||||
Vec3f( 0.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -1.0),
|
||||
Vec3f(-1.0, 0.0, 0.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f( 0.0, 0.0, -1.0),
|
||||
Vec3f( 0.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -1.0),
|
||||
Vec3f(-1.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, 0.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f( 0.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -1.0),
|
||||
Vec3f(-1.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, -1.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(-1.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -1.0),
|
||||
Vec3f(-1.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, -1.0),
|
||||
Vec3f( 0.0, 0.0, -2.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(-1.0, 0.0, -1.0),
|
||||
Vec3f(-1.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, -1.0),
|
||||
Vec3f( 0.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -2.0),
|
||||
]))
|
||||
assert(Self.isRectangle([
|
||||
Vec3f(-1.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, 0.0),
|
||||
Vec3f( 0.0, 0.0, -1.0),
|
||||
Vec3f( 0.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -2.0),
|
||||
Vec3f(-1.0, 0.0, -1.0),
|
||||
]))
|
||||
}
|
||||
|
||||
private static let epsilon: Float = 0.00001 //.ulpOfOne
|
||||
|
||||
private static func isSimpleQuad(_ p: (Vec3f, Vec3f, Vec3f, Vec3f)) -> Bool
|
||||
{
|
||||
if abs(p.0.y - p.1.y) <= epsilon { return abs(p.2.y - p.3.y) <= epsilon }
|
||||
if abs(p.1.y - p.2.y) <= epsilon { return abs(p.3.y - p.0.y) <= epsilon }
|
||||
return false
|
||||
}
|
||||
|
||||
private static func isRectangle(_ p: (Vec3f, Vec3f, Vec3f, Vec3f)) -> Bool
|
||||
{
|
||||
if abs(p.0.x - p.1.x) <= epsilon &&
|
||||
abs(p.1.z - p.2.z) <= epsilon &&
|
||||
abs(p.2.x - p.3.x) <= epsilon &&
|
||||
abs(p.3.z - p.0.z) <= epsilon { return true }
|
||||
if abs(p.0.z - p.1.z) <= epsilon &&
|
||||
abs(p.1.x - p.2.x) <= epsilon &&
|
||||
abs(p.2.z - p.3.z) <= epsilon &&
|
||||
abs(p.3.x - p.0.x) <= epsilon { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
private static func isRectangle(_ positions: [Vec3f]) -> Bool
|
||||
{
|
||||
var winding: Winding = .none
|
||||
var xdir: Float = 0.0, zdir: Float = 0.0
|
||||
let first = positions[0]
|
||||
var previous = first
|
||||
for p in positions[1...]
|
||||
{
|
||||
let (xdelta, zdelta) = (p.x - previous.x, p.z - previous.z)
|
||||
let (xzero, zzero) = (abs(xdelta) <= epsilon, abs(zdelta) <= epsilon)
|
||||
if !xzero && zzero
|
||||
{
|
||||
if xdir != 0.0 && xdelta.sign != xdir.sign { return false }
|
||||
if zdir != 0.0
|
||||
{
|
||||
switch winding
|
||||
{
|
||||
case .none: winding = xdelta.sign == zdir.sign ? .ccw : .cw
|
||||
case .cw: if xdelta.sign == zdir.sign { return false }
|
||||
case .ccw: if xdelta.sign != zdir.sign { return false }
|
||||
}
|
||||
}
|
||||
(xdir, zdir) = (xdelta, 0.0)
|
||||
}
|
||||
else if xzero && !zzero
|
||||
{
|
||||
if zdir != 0.0 && zdelta.sign != zdir.sign { return false }
|
||||
if xdir != 0.0
|
||||
{
|
||||
switch winding
|
||||
{
|
||||
case .none: winding = zdelta.sign == xdir.sign ? .cw : .ccw
|
||||
case .cw: if zdelta.sign != xdir.sign { return false }
|
||||
case .ccw: if zdelta.sign == xdir.sign { return false }
|
||||
}
|
||||
}
|
||||
(xdir, zdir) = (0.0, zdelta)
|
||||
}
|
||||
else if !xzero && !zzero { return false }
|
||||
previous = p
|
||||
}
|
||||
return abs(first.x - previous.x) <= epsilon || abs(first.z - previous.z) <= epsilon
|
||||
}
|
||||
|
||||
private static func getQuadWinding(_ p: (Vec3f, Vec3f, Vec3f, Vec3f)) -> Winding
|
||||
{
|
||||
var area: Float = 0.0
|
||||
area += (p.1.x - p.0.x) * ((p.0.z + p.1.z) * 0.5)
|
||||
area += (p.2.x - p.1.x) * ((p.1.z + p.2.z) * 0.5)
|
||||
area += (p.3.x - p.2.x) * ((p.2.z + p.3.z) * 0.5)
|
||||
area += (p.0.x - p.3.x) * ((p.3.z + p.0.z) * 0.5)
|
||||
return area.sign == .plus ? .ccw : .cw // z is towards us
|
||||
}
|
||||
|
||||
static func quadSpaceFromCartesian(quad: (Vec3f, Vec3f, Vec3f, Vec3f), position: Vec3f) -> Vec2f
|
||||
{
|
||||
let p = (
|
||||
Vec2d(Double(quad.0.x), Double(quad.0.z)),
|
||||
Vec2d(Double(quad.1.x), Double(quad.1.z)),
|
||||
Vec2d(Double(quad.2.x), Double(quad.2.z)),
|
||||
Vec2d(Double(quad.3.x), Double(quad.3.z)))
|
||||
|
||||
let xz = Vec2d(Double(position.x), Double(position.z))
|
||||
|
||||
/*
|
||||
let old = {
|
||||
let a = xz.x - p.0.x
|
||||
let b = p.1.x - p.0.x
|
||||
let c = p.3.x - p.0.x
|
||||
let d = p.0.x - p.1.x + p.2.x - p.3.x
|
||||
let f = xz.y - p.0.y
|
||||
let g = p.1.y - p.0.y
|
||||
let h = p.3.y - p.0.y
|
||||
let j = p.0.y - p.1.y + p.2.y - p.3.y
|
||||
let v2 = -c * j - (-d * h)
|
||||
let v1 = a * j - c * g - (d * f - b * h)
|
||||
let v0 = a * g - b * f
|
||||
let vq = (-v1 + sqrt(v1 * v1 - 4.0 * v2 * v0)) / (2.0 * v2)
|
||||
let uq = (a - c * vq) / (b + d * vq)
|
||||
return Vec2f(Float(uq), Float(vq))
|
||||
}
|
||||
*/
|
||||
|
||||
let a = xz - p.0
|
||||
let b = p.1 - p.0
|
||||
let c = p.3 - p.0
|
||||
let d = p.0 - p.1 + p.2 - p.3
|
||||
let v0 = a.cross(b), v2 = c.cross(-d)
|
||||
let v1 = a.x * d.y - b.y * c.x - (a.y * d.x - b.x * c.y)
|
||||
let vq = (-v1 + sqrt(v1 * v1 - 4.0 * v2 * v0)) / (2.0 * v2)
|
||||
let uq = (a.x - c.x * vq) / (b.x + d.x * vq)
|
||||
return Vec2f(Float(uq), Float(vq))
|
||||
//let oldUv = old()
|
||||
//if !oldUv.x.isNaN || !uv.x.isNaN { assert (oldUv.x == uv.x) }
|
||||
//if !oldUv.y.isNaN || !uv.y.isNaN { assert (oldUv.y == uv.y) }
|
||||
//return uv
|
||||
}
|
||||
|
||||
enum Winding { case none, cw, ccw }
|
||||
|
||||
func build(obj: ObjModel, collision: ObjModel.Object)
|
||||
{
|
||||
for face in collision.faces
|
||||
{
|
||||
switch face
|
||||
{
|
||||
case .triangle(let v1, let v2, let v3):
|
||||
let p = (obj.positions[v1.p], obj.positions[v2.p], obj.positions[v3.p])
|
||||
let n = (obj.normals[v1.n] + obj.normals[v2.n] + obj.normals[v3.n]).normalised
|
||||
if abs(n.y) < 0.25 { continue }
|
||||
edge3d.append(.triangle(n: n, p: (p.0 + p.1 + p.2) / 3.0, v: p))
|
||||
case .quad(let v1, let v2, let v3, let v4):
|
||||
let p = (obj.positions[v1.p], obj.positions[v2.p], obj.positions[v3.p], obj.positions[v4.p])
|
||||
let n = (obj.normals[v1.n] + obj.normals[v2.n] + obj.normals[v3.n] + obj.normals[v4.n]).normalised
|
||||
if Self.isSimpleQuad(p) && Self.isRectangle(p)
|
||||
{
|
||||
if abs(n.y) < 0.25 { continue }
|
||||
let left = min(p.0.x, p.1.x, p.2.x, p.3.x)
|
||||
let right = max(p.0.x, p.1.x, p.2.x, p.3.x)
|
||||
let back = min(p.0.z, p.1.z, p.2.z, p.3.z)
|
||||
let forward = max(p.0.z, p.1.z, p.2.z, p.3.z)
|
||||
edge3d.append(.aabbFloor(n: n,
|
||||
p: (p.0 + p.1 + p.2 + p.3) / 4.0,
|
||||
w: (right - left) / 2.0,
|
||||
d: (forward - back) / 2.0))
|
||||
}
|
||||
else
|
||||
{
|
||||
if abs(n.y) < 0.25 { continue }
|
||||
edge3d.append(.quad(p: p, w: Self.getQuadWinding(p)))
|
||||
/*
|
||||
edge3d.append(.triangle(n: n, p: (p.0 + p.1 + p.2) / 3.0, v: (p.0, p.1, p.2)))
|
||||
edge3d.append(.triangle(n: n, p: (p.2 + p.3 + p.0) / 3.0, v: (p.2, p.3, p.0)))
|
||||
*/
|
||||
}
|
||||
case .ngon(let v):
|
||||
let p = v.map { obj.positions[$0.p] }
|
||||
let n = v.reduce(.zero) { $0 + obj.normals[$1.n] }.normalised
|
||||
if abs(n.y) < 0.25 { continue }
|
||||
if Self.isRectangle(p)
|
||||
{
|
||||
let left = p.map { $0.x }.min()!, right = p.map { $0.x }.max()!
|
||||
let bottom = p.map { $0.y }.min()!, top = p.map { $0.y }.max()!
|
||||
let back = p.map { $0.z }.min()!, forward = p.map { $0.z }.max()!
|
||||
let position = Vec3f(left + right, bottom + top, back + forward) / 2.0
|
||||
edge3d.append(.aabbFloor(n: n, p: position,
|
||||
w: (right - left) / 2.0,
|
||||
d: (forward - back) / 2.0))
|
||||
}
|
||||
else
|
||||
{
|
||||
let p0 = p[0]
|
||||
for i in 1..<(v.count-1)
|
||||
{
|
||||
let p1 = p[i], p2 = p[i + 1]
|
||||
edge3d.append(.triangle(n: n, p: (p0 + p1 + p2) / 3.0, v: (p0, p1, p2)))
|
||||
}
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func draw(_ render: Renderer, position: Vec3f)
|
||||
{
|
||||
var lines = [Line](
|
||||
repeating: .init(from: .init(), to: .init(), colour: .zero),
|
||||
count: edge3d.count * 6)
|
||||
var i: Int = 0
|
||||
for edge in edge3d
|
||||
{
|
||||
var o: Vec3f = .zero, n: Vec3f = .zero
|
||||
switch edge
|
||||
{
|
||||
case .triangle(let trin, let trip, let triv):
|
||||
o = trip
|
||||
n = trin
|
||||
|
||||
let v0 = triv.1 - triv.0;
|
||||
let v1 = triv.2 - triv.0;
|
||||
let v2 = position - triv.0;
|
||||
let iden = 1.0 / (v0.x * v1.z - v1.x * v0.z);
|
||||
let v = (v2.x * v1.z - v1.x * v2.z) * iden;
|
||||
let w = (v0.x * v2.z - v2.x * v0.z) * iden;
|
||||
let colour = if v >= 0.0 && w >= 0.0 && v + w <= 1.0
|
||||
{ XnaColour.Red } else { XnaColour.GreenYellow }
|
||||
//let p = triv
|
||||
//let det = (p.1.x - p.0.x) * (p.2.z - p.0.z) - (p.1.z - p.0.z) * (p.2.x - p.0.x)
|
||||
//let colour: Colour = if
|
||||
// det * (p.1.x - p.0.x) * (position.z - p.0.z) - (p.1.z - p.0.z) * (position.x - p.0.x) >= 0,
|
||||
// det * (p.2.x - p.1.x) * (position.z - p.1.z) - (p.2.z - p.1.z) * (position.x - p.1.x) >= 0,
|
||||
// det * (p.0.x - p.2.x) * (position.z - p.2.z) - (p.0.z - p.2.z) * (position.x - p.2.x) >= 0
|
||||
/*
|
||||
let side = { (v1: Vec3f, v2: Vec3f, p: Vec3f) in
|
||||
(v2.z - v1.z) * (p.x - v1.x) + (v2.x + v1.x) * (position.z - v1.z) }
|
||||
let colour = if
|
||||
side(triv.0, triv.1, position) >= 0,
|
||||
side(triv.1, triv.2, position) >= 0,
|
||||
side(triv.2, triv.0, position) >= 0
|
||||
*/
|
||||
|
||||
lines[i + 0] = Line(from: triv.0, to: triv.1, colour: colour)
|
||||
lines[i + 1] = Line(from: triv.1, to: triv.2, colour: colour)
|
||||
lines[i + 2] = Line(from: triv.2, to: triv.0, colour: colour)
|
||||
i += 3
|
||||
case .aabbFloor(let floorn, let floorp, let floorw, let floord):
|
||||
o = floorp
|
||||
n = floorn
|
||||
|
||||
let n2 = Vec2f(n.x, n.z) / n.y
|
||||
let z0 = Vec2f(-floorw, floord).dot(n2)
|
||||
let z1 = Vec2f(-floorw, -floord).dot(n2)
|
||||
let z2 = Vec2f( floorw, -floord).dot(n2)
|
||||
let z3 = Vec2f( floorw, floord).dot(n2)
|
||||
|
||||
let c0 = floorp + Vec3f( floorw, z0, -floord)
|
||||
let c1 = floorp + Vec3f( floorw, z1, floord)
|
||||
let c2 = floorp + Vec3f(-floorw, z2, floord)
|
||||
let c3 = floorp + Vec3f(-floorw, z3, -floord)
|
||||
|
||||
lines[i + 0] = Line(from: c0, to: c1, colour: XnaColour.GreenYellow)
|
||||
lines[i + 1] = Line(from: c1, to: c2, colour: XnaColour.GreenYellow)
|
||||
lines[i + 2] = Line(from: c2, to: c3, colour: XnaColour.GreenYellow)
|
||||
lines[i + 3] = Line(from: c3, to: c0, colour: XnaColour.GreenYellow)
|
||||
i += 4
|
||||
case .quad(let verts, let winding):
|
||||
/*
|
||||
let p = (
|
||||
verts.0 + (verts.1 - verts.0) * position.x,
|
||||
verts.3 + (verts.2 - verts.3) * position.x,
|
||||
verts.0 + (verts.3 - verts.0) * position.z,
|
||||
verts.1 + (verts.2 - verts.1) * position.z)
|
||||
|
||||
let xdiff = Vec2f(p.0.x - p.1.x, p.2.x - p.3.x)
|
||||
let ydiff = Vec2f(p.0.z - p.1.z, p.2.z - p.3.z)
|
||||
let div = xdiff.cross(ydiff)
|
||||
guard div != 0.0 else { break }
|
||||
|
||||
let d = Vec2f(
|
||||
Vec2f(p.0.x, p.0.z).cross(Vec2f(p.1.x, p.1.z)),
|
||||
Vec2f(p.2.x, p.2.z).cross(Vec2f(p.3.x, p.3.z)))
|
||||
let pos = Vec2f(d.cross(xdiff), d.cross(ydiff)) / div
|
||||
|
||||
o = Vec3f(pos.x, 0.0, pos.y)
|
||||
n = .up
|
||||
*/
|
||||
|
||||
//n = winding == .ccw ? .up : .down
|
||||
o = verts.0.lerp(verts.3, 0.5).lerp(
|
||||
verts.2.lerp(verts.1, 0.5), 0.5)
|
||||
|
||||
/*
|
||||
let xy = Vec2d(Double(position.x), Double(position.z))
|
||||
let a = xy.x - p.0.x
|
||||
let b = p.1.x - p.0.x
|
||||
let c = p.3.x - p.0.x
|
||||
let d = p.0.x - p.1.x + p.2.x - p.3.x
|
||||
let f = xy.y - p.0.y
|
||||
let g = p.1.y - p.0.y
|
||||
let h = p.3.y - p.0.y
|
||||
let j = p.0.y - p.1.y + p.2.y - p.3.y
|
||||
let v2 = -c * j - (-d * h)
|
||||
let v1 = a * j - c * g - (d * f - b * h)
|
||||
let v0 = a * g - b * f
|
||||
let vq = (-v1 + sqrt(v1 * v1 - 4.0 * v2 * v0)) / (2.0 * v2)
|
||||
let uq = (a - c * vq) / (b + d * vq)
|
||||
let uv = Vec2d(uq, vq)
|
||||
*/
|
||||
|
||||
var colour = XnaColour.GreenYellow
|
||||
|
||||
/*
|
||||
p1 p2
|
||||
*------*
|
||||
| |
|
||||
| |
|
||||
*------*
|
||||
p0 p3
|
||||
*/
|
||||
|
||||
let p = winding == .ccw ? (verts.3, verts.2, verts.1, verts.0) : (verts.0, verts.1, verts.2, verts.3)
|
||||
let vn = (
|
||||
(verts.1 - verts.0).cross(verts.3 - verts.0),
|
||||
(verts.2 - verts.1).cross(verts.0 - verts.1),
|
||||
(verts.3 - verts.2).cross(verts.1 - verts.2),
|
||||
(verts.0 - verts.3).cross(verts.2 - verts.3)
|
||||
)
|
||||
n = (vn.0 + vn.1 + vn.2 + vn.3).normalised
|
||||
|
||||
let uv = Self.quadSpaceFromCartesian(quad: p, position: position)
|
||||
if uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0
|
||||
//if uv.x >= -1.0 && uv.x <= 1.0 && uv.y >= -1.0 && uv.y <= 1.0
|
||||
{
|
||||
var pp = p.0
|
||||
pp += (p.1 - p.0) * uv.x
|
||||
pp += (p.3 - p.0) * uv.y
|
||||
pp += (p.0 - p.1 + p.2 - p.3) * uv.x * uv.y
|
||||
lines[i] = Line(from: o, to: pp, colour: XnaColour.Aquamarine)
|
||||
i += 1
|
||||
colour = XnaColour.BurlyWood
|
||||
o = pp
|
||||
n = vn.0.lerp(vn.3, uv.x).lerp(
|
||||
vn.2.lerp(vn.1, uv.x), winding == .cw ? uv.y : 1.0 - uv.y).normalised
|
||||
}
|
||||
|
||||
lines[i + 0] = Line(from: verts.0, to: verts.1, colour: colour)
|
||||
lines[i + 1] = Line(from: verts.1, to: verts.2, colour: colour)
|
||||
lines[i + 2] = Line(from: verts.2, to: verts.3, colour: colour)
|
||||
lines[i + 3] = Line(from: verts.3, to: verts.0, colour: colour)
|
||||
i += 4
|
||||
}
|
||||
let p = position
|
||||
let a = p.dot(n) - o.dot(n)
|
||||
let pissy = p - n * a
|
||||
|
||||
//let d = colin.position - edge.p
|
||||
//let pissy = edge.p + d.cross(edge.n)
|
||||
|
||||
/*
|
||||
if a > -1.0 && a <= 0.0
|
||||
{
|
||||
lines[i + 0] = Line(from: o, to: o + n * max(0, a), colour: XnaColour.Magenta)
|
||||
lines[i + 1] = Line(from: pissy + .up, to: o, colour: XnaColour.Red)
|
||||
i += 2
|
||||
}
|
||||
*/
|
||||
lines[i + 0] = Line(from: o, to: o + n * 0.2, colour: XnaColour.Magenta)
|
||||
i += 1
|
||||
//lines[i + 4] = Line(from: pissy + Vec3f(-1, 0, 0) * 0.1, to: pissy + Vec3f(1, 0, 0) * 0.1, colour: XnaColour.Red)
|
||||
//lines[i + 5] = Line(from: pissy + Vec3f( 0,-1, 0) * 0.1, to: pissy + Vec3f(0, 1, 0) * 0.1, colour: XnaColour.Red)
|
||||
//lines[i + 6] = Line(from: pissy + Vec3f( 0, 0,-1) * 0.1, to: pissy + Vec3f(0, 0, 1) * 0.1, colour: XnaColour.Red)
|
||||
|
||||
}
|
||||
render.drawGizmos(lines: lines)
|
||||
}
|
||||
}
|
9
Sources/Test/Objects/Actor.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import JolkEngine
|
||||
|
||||
protocol Actor
|
||||
{
|
||||
func update(deltaTime: Float, world: Collision)
|
||||
|
||||
var position: Vec3f { get }
|
||||
var transform: Mat4f { get }
|
||||
}
|
276
Sources/Test/Objects/Colin.swift
Normal file
@ -0,0 +1,276 @@
|
||||
import SDL2
|
||||
import simd
|
||||
import OpenGL.GL
|
||||
import JolkEngine
|
||||
|
||||
|
||||
class Colin: Actor
|
||||
{
|
||||
var position: Vec3f { _pos }
|
||||
|
||||
var transform: Mat4f
|
||||
{
|
||||
//.rotate(yaw: angle.x, pitch: angle.y, roll: sin(time)) *
|
||||
//.scale(Vec3f(1.0 + 0.25 * cos(time), 1.0 + 0.25 * sin(time), 1.0)) *
|
||||
.rotate(yawPitch: angle) * .translate(-position - Vec3f(0, 1, 0))
|
||||
}
|
||||
|
||||
var angle: Vec2f { return Vec2f(ofsAngle.x + _angle, ofsAngle.y) }
|
||||
var fov: Float { return _fov }
|
||||
|
||||
private var time: Float = 0.0
|
||||
//private var jumpVel: Float = 0.0
|
||||
private var velocity: Vec3f = .zero
|
||||
private var _pos: Vec3f = .zero
|
||||
private var _pos2D: Vec2f { get { Vec2f(_pos.x, _pos.z) } set(new) { _pos.x = new.x; _pos.z = new.y } }
|
||||
private var _angle: Float = 0.0
|
||||
private var ofsAngle: Vec2f = .zero
|
||||
private var _fov: Float = 60.0
|
||||
|
||||
private var nutted = false
|
||||
private var colinMode = false
|
||||
private var backPressed = false
|
||||
|
||||
private let colinWidth: Float = 0.2
|
||||
|
||||
private func move2D(new: Vec2f, edges: [Collision.Edge])
|
||||
{
|
||||
let velocity = new - _pos2D
|
||||
var lastPos = _pos2D
|
||||
_pos2D = new
|
||||
|
||||
var collided = -1
|
||||
jolk: while (collided != 0)
|
||||
{
|
||||
let scabidabadoo = 10
|
||||
for edge in edges
|
||||
{
|
||||
let diff = _pos2D - lastPos
|
||||
if simd_dot(edge.n, diff) > 0 && simd_dot(edge.n, velocity) > 0 { continue }
|
||||
|
||||
let deltaPos = _pos2D - edge.p
|
||||
let something = deltaPos.cross(edge.n)
|
||||
if abs(something) * 2.0 < edge.w
|
||||
{
|
||||
let dot = simd_dot(edge.n, deltaPos)
|
||||
if dot > 0 && dot < colinWidth
|
||||
{
|
||||
lastPos = _pos2D
|
||||
_pos2D += edge.n * (colinWidth - dot)
|
||||
if collided < 0 { collided = scabidabadoo };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let vec = (deltaPos - Vec2f(edge.n.y, -edge.n.x) * Float(signOf: something, magnitudeOf: edge.w * 0.5))
|
||||
let distance2 = vec.len2
|
||||
let epsilon: Float = 0.001
|
||||
if distance2 < (colinWidth - epsilon) * (colinWidth - epsilon)
|
||||
{
|
||||
let distance = distance2.squareRoot()
|
||||
lastPos = _pos2D
|
||||
_pos2D += (vec / distance) * (colinWidth - epsilon - distance)
|
||||
if collided < 0 { collided = scabidabadoo };
|
||||
}
|
||||
}
|
||||
}
|
||||
collided -= 1;
|
||||
if collided <= 0 { break jolk }
|
||||
}
|
||||
}
|
||||
|
||||
private func response3D(plainPos o: Vec3f, plainNorm n: Vec3f)
|
||||
{
|
||||
let height: Float = 1.12
|
||||
//let diff = _pos - lastPos
|
||||
|
||||
//let p = n.y > 1.0 ? _pos : (_pos + 2.0)
|
||||
let ofs: Vec3f = n.y.sign == .plus ? .zero : .up * height
|
||||
//let p = if n.y.sign == .plus { _pos } else { _pos + 1.0 }
|
||||
let p = _pos + ofs
|
||||
let a = p.dot(n) - o.dot(n)
|
||||
|
||||
if a > height * -0.5 && a < 0.0
|
||||
{
|
||||
_pos -= n * a
|
||||
velocity -= velocity.project(n)
|
||||
}
|
||||
}
|
||||
|
||||
private func move3D(_ deltaTime: Float, world: Collision)
|
||||
{
|
||||
var lastPos = _pos
|
||||
_pos += velocity * deltaTime
|
||||
|
||||
for edge in world.edge3d
|
||||
{
|
||||
switch edge
|
||||
{
|
||||
case .aabbFloor(let normal, let origin, let width, let depth):
|
||||
//let left = min(edge.v0.x, edge.v1.x, edge.v2.x)
|
||||
//let right = max(edge.v0.x, edge.v1.x, edge.v2.x)
|
||||
//let back = min(edge.v0.z, edge.v1.z, edge.v2.z)
|
||||
//let forward = max(edge.v0.z, edge.v1.z, edge.v2.z)
|
||||
|
||||
let left = origin.x - width, right = origin.x + width
|
||||
let back = origin.z - depth, forward = origin.z + depth
|
||||
|
||||
if _pos.x < left || _pos.x > right { continue }
|
||||
if _pos.z < back || _pos.z > forward { continue }
|
||||
|
||||
response3D(plainPos: origin, plainNorm: normal)
|
||||
|
||||
case .triangle(let normal, let origin, let verts):
|
||||
let p = (verts.1 - verts.0, verts.2 - verts.0, _pos - verts.0)
|
||||
let invDenom = 1.0 / (p.0.x * p.1.z - p.1.x * p.0.z);
|
||||
let v = invDenom * (p.2.x * p.1.z - p.1.x * p.2.z);
|
||||
let w = invDenom * (p.0.x * p.2.z - p.2.x * p.0.z);
|
||||
let x = 1.0 - v - w
|
||||
//guard v >= 0.0 && w >= 0.0 && v + w <= 1.0 else { break }
|
||||
guard v >= 0.0 && w >= 0.0 && 0 <= x && x <= 1.0 else { break }
|
||||
response3D(plainPos: origin, plainNorm: normal)
|
||||
|
||||
case .quad(let verts, let winding):
|
||||
let vn = (
|
||||
(verts.1 - verts.0).cross(verts.3 - verts.0),
|
||||
(verts.2 - verts.1).cross(verts.0 - verts.1),
|
||||
(verts.3 - verts.2).cross(verts.1 - verts.2),
|
||||
(verts.0 - verts.3).cross(verts.2 - verts.3)
|
||||
)
|
||||
|
||||
let uv = Collision.quadSpaceFromCartesian(quad:
|
||||
winding == .ccw ? (verts.3, verts.2, verts.1, verts.0) : (verts.0, verts.1, verts.2, verts.3)
|
||||
, position: _pos)
|
||||
if uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0
|
||||
{
|
||||
//var pp = p.0
|
||||
//pp += (p.1 - p.0) * uv.x
|
||||
//pp += (p.3 - p.0) * uv.y
|
||||
//pp += (p.0 - p.1 + p.2 - p.3) * uv.x * uv.y
|
||||
let p = verts.0.lerp(verts.1, uv.x).lerp(
|
||||
verts.3.lerp(verts.2, uv.x), winding == .cw ? uv.y : 1.0 - uv.y)
|
||||
let n = vn.0.lerp(vn.3, uv.x).lerp(
|
||||
vn.2.lerp(vn.1, uv.x), winding == .cw ? uv.y : 1.0 - uv.y).normalised
|
||||
|
||||
response3D(plainPos: Vec3f(_pos.x, p.y, _pos.z), plainNorm: n)
|
||||
}
|
||||
/*
|
||||
let p = (
|
||||
verts.0 + (verts.1 - verts.0) * _pos.x,
|
||||
verts.3 + (verts.2 - verts.3) * _pos.x,
|
||||
verts.0 + (verts.3 - verts.0) * _pos.z,
|
||||
verts.1 + (verts.2 - verts.1) * _pos.z)
|
||||
|
||||
let xdiff = Vec2f(p.0.x - p.1.x, p.2.x - p.3.x)
|
||||
let ydiff = Vec2f(p.0.z - p.1.z, p.2.z - p.3.z)
|
||||
let div = xdiff.cross(ydiff)
|
||||
guard div != 0.0 else { break }
|
||||
|
||||
let d = Vec2f(
|
||||
Vec2f(p.0.x, p.0.z).cross(Vec2f(p.1.x, p.1.z)),
|
||||
Vec2f(p.2.x, p.2.z).cross(Vec2f(p.3.x, p.3.z)))
|
||||
let pos = Vec2f(d.cross(xdiff), d.cross(ydiff)) / div
|
||||
*/
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(deltaTime: Float, world: Collision)
|
||||
{
|
||||
time += deltaTime
|
||||
|
||||
let axisRescale = 1.0 / Float(INT16_MAX)
|
||||
let getAxis = { (axis: SDL_GameControllerAxis) in
|
||||
Float(SDL_GameControllerGetAxis(sdlPad, axis)) * axisRescale }
|
||||
let getButton = { (btn: SDL_GameControllerButton) in
|
||||
SDL_GameControllerGetButton(sdlPad, btn) == SDL_PRESSED }
|
||||
|
||||
let stick = Vec2f(
|
||||
getAxis(SDL_CONTROLLER_AXIS_LEFTX),
|
||||
getAxis(SDL_CONTROLLER_AXIS_LEFTY))
|
||||
.cardinalDeadzone(min: 0.1, max: 1)
|
||||
|
||||
let turnSpeed = Float.pi * 2 * 0.1
|
||||
if Input.instance.keyDown(.left)
|
||||
{
|
||||
_angle -= turnSpeed * deltaTime
|
||||
}
|
||||
if Input.instance.keyDown(.right)
|
||||
{
|
||||
_angle += turnSpeed * deltaTime
|
||||
}
|
||||
if stick != .zero
|
||||
{
|
||||
_angle += stick.x * 2.0 * turnSpeed * deltaTime
|
||||
}
|
||||
|
||||
var moveVec: Vec2f = .zero
|
||||
|
||||
let speed: Float = 2
|
||||
let forward = Vec2f(sin(self._angle), -cos(self._angle))
|
||||
if Input.instance.keyDown(.up)
|
||||
{
|
||||
moveVec += forward * speed
|
||||
}
|
||||
if Input.instance.keyDown(.down)
|
||||
{
|
||||
moveVec -= forward * speed
|
||||
}
|
||||
if stick != .zero
|
||||
{
|
||||
moveVec -= forward * stick.y * speed
|
||||
}
|
||||
|
||||
if Input.instance.keyDown(.c)
|
||||
{
|
||||
colinMode = !colinMode
|
||||
}
|
||||
|
||||
let lookCone = Float.pi / 2.0
|
||||
let dst = Vec2f(
|
||||
getAxis(SDL_CONTROLLER_AXIS_RIGHTX),
|
||||
getAxis(SDL_CONTROLLER_AXIS_RIGHTY))
|
||||
.radialDeadzone(min: 0.1, max: 1) * lookCone
|
||||
ofsAngle = ofsAngle.lerp(dst, 16 * deltaTime)
|
||||
|
||||
let targetFov = Float.lerp(60, 20, getAxis(SDL_CONTROLLER_AXIS_TRIGGERRIGHT))
|
||||
_fov = Float.lerp(_fov, targetFov, 20 * deltaTime)
|
||||
|
||||
let right = Vec2f(cos(self._angle), sin(self._angle))
|
||||
if getButton(SDL_CONTROLLER_BUTTON_LEFTSHOULDER)
|
||||
{
|
||||
moveVec -= right * speed
|
||||
}
|
||||
if getButton(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)
|
||||
{
|
||||
moveVec += right * speed
|
||||
}
|
||||
|
||||
let back = getButton(SDL_CONTROLLER_BUTTON_BACK)
|
||||
if !backPressed && back { colinMode = !colinMode }
|
||||
backPressed = back
|
||||
|
||||
if getButton(SDL_CONTROLLER_BUTTON_A) || Input.instance.keyDown(.n)
|
||||
{
|
||||
// play nut.wav
|
||||
nutted = true
|
||||
}
|
||||
else { nutted = false }
|
||||
|
||||
//move2D(new: _pos2D + velocity, edges: edges)
|
||||
|
||||
if Input.instance.keyDown(.z) || getButton(SDL_CONTROLLER_BUTTON_B) { velocity.y = 2.0 }
|
||||
//_pos.y += jumpVel * deltaTime
|
||||
//if _pos.y > 0.0 { jumpVel -= 5.4 * deltaTime }
|
||||
velocity.y -= 5.4 * deltaTime
|
||||
velocity.x = moveVec.x
|
||||
velocity.z = moveVec.y
|
||||
move3D(deltaTime, world: world)
|
||||
//else if _pos.y < 0.0
|
||||
//{
|
||||
// jumpVel = max(jumpVel, 0.0)
|
||||
// _pos.y = 0.0
|
||||
//}
|
||||
}
|
||||
}
|
33
Sources/Test/Objects/JolkCube.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
import simd
|
||||
import JolkEngine
|
||||
|
||||
|
||||
class JolkCube: Actor
|
||||
{
|
||||
private var _pos: Vec3f
|
||||
private var theta: Float = 0.0
|
||||
|
||||
init(position: Vec3f)
|
||||
{
|
||||
_pos = position
|
||||
}
|
||||
|
||||
var position: Vec3f { _pos }
|
||||
|
||||
func update(deltaTime: Float, world: Collision)
|
||||
{
|
||||
theta += 15 * deltaTime
|
||||
_pos.y = 1 + sin(theta * 0.25) * 0.25
|
||||
}
|
||||
|
||||
var transform: Mat4f
|
||||
{
|
||||
.translate(_pos) *
|
||||
.rotate(x: theta * 0.25) *
|
||||
.rotate(y: Float.rad(fromDeg: theta)) *
|
||||
// .rotate(axis: .X, angle: theta * 0.25) *
|
||||
// .rotate(axis: .Y, angle: Float.rad(fromDeg: theta)) *
|
||||
.scale(scalar: 0.25)
|
||||
}
|
||||
}
|
33
Sources/Test/Resources/Models/World.mtl
Normal file
@ -0,0 +1,33 @@
|
||||
# Blender 3.6.2 MTL File: 'World.blend'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Cobblestone
|
||||
Ns 437.449280
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 1
|
||||
map_Kd cobblestone.png
|
||||
map_Ks cobblestone_specular.png
|
||||
map_Bump -bm 1.000000 cobblestone_normal.png
|
||||
|
||||
newmtl Material
|
||||
Ns 167.965591
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.089179 0.800000 0.000000
|
||||
Ks 0.483607 0.483607 0.483607
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 2
|
||||
|
||||
newmtl MetalWall
|
||||
Ns 55.588612
|
||||
Ka 0.390244 0.390244 0.390244
|
||||
Ks 0.573171 0.573171 0.573171
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 9.650000
|
||||
d 1.000000
|
||||
illum 3
|
||||
map_Kd metalwall1.jpg
|
13583
Sources/Test/Resources/Models/World.obj
Normal file
BIN
Sources/Test/Resources/Models/suzanne.g3db
Normal file
BIN
Sources/Test/Resources/Models/toybox.g3db
Normal file
BIN
Sources/Test/Resources/Textures/cobblestone.png
Normal file
After Width: | Height: | Size: 3.3 MiB |
BIN
Sources/Test/Resources/Textures/cobblestone_bump.png
Normal file
After Width: | Height: | Size: 197 KiB |
BIN
Sources/Test/Resources/Textures/cobblestone_normal.png
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
Sources/Test/Resources/Textures/cobblestone_specular.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
Sources/Test/Resources/Textures/colin.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
Sources/Test/Resources/Textures/jolkmeup.jpg
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
Sources/Test/Resources/Textures/metalwall1.jpg
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
Sources/Test/Resources/Textures/suzanne_diffuse.png
Normal file
After Width: | Height: | Size: 656 KiB |
BIN
Sources/Test/Resources/Textures/suzanne_normal.png
Normal file
After Width: | Height: | Size: 881 KiB |
BIN
Sources/Test/Resources/Textures/toybox_albedo.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
Sources/Test/Resources/Textures/toybox_displace.png
Normal file
After Width: | Height: | Size: 145 KiB |
BIN
Sources/Test/Resources/Textures/toybox_normal.png
Normal file
After Width: | Height: | Size: 231 KiB |
BIN
Sources/Test/World.blend
Normal file
148
Sources/Test/XnaColour.swift
Normal file
@ -0,0 +1,148 @@
|
||||
import JolkEngine
|
||||
|
||||
|
||||
public struct XnaColour
|
||||
{
|
||||
public static let Transparent = Colour.zero
|
||||
public static let AliceBlue = Colour(fromRgb24: 0xF0F8FF)
|
||||
public static let AntiqueWhite = Colour(fromRgb24: 0xFAEBD7)
|
||||
public static let Aqua = Colour(fromRgb24: 0x00FFFF)
|
||||
public static let Aquamarine = Colour(fromRgb24: 0x7FFFD4)
|
||||
public static let Azure = Colour(fromRgb24: 0xF0FFFF)
|
||||
public static let Beige = Colour(fromRgb24: 0xF5F5DC)
|
||||
public static let Bisque = Colour(fromRgb24: 0xFFE4C4)
|
||||
public static let Black = Colour.black
|
||||
public static let BlanchedAlmond = Colour(fromRgb24: 0xFFEBCD)
|
||||
public static let Blue = Colour(fromRgb24: 0x0000FF)
|
||||
public static let BlueViolet = Colour(fromRgb24: 0x8A2BE2)
|
||||
public static let Brown = Colour(fromRgb24: 0xA52A2A)
|
||||
public static let BurlyWood = Colour(fromRgb24: 0xDEB887)
|
||||
public static let CadetBlue = Colour(fromRgb24: 0x5F9EA0)
|
||||
public static let Chartreuse = Colour(fromRgb24: 0x7FFF00)
|
||||
public static let Chocolate = Colour(fromRgb24: 0xD2691E)
|
||||
public static let Coral = Colour(fromRgb24: 0xFF7F50)
|
||||
public static let CornflowerBlue = Colour(fromRgb24: 0x6495ED)
|
||||
public static let Cornsilk = Colour(fromRgb24: 0xFFF8DC)
|
||||
public static let Crimson = Colour(fromRgb24: 0xDC143C)
|
||||
public static let Cyan = Colour(fromRgb24: 0x00FFFF)
|
||||
public static let DarkBlue = Colour(fromRgb24: 0x00008B)
|
||||
public static let DarkCyan = Colour(fromRgb24: 0x008B8B)
|
||||
public static let DarkGoldenrod = Colour(fromRgb24: 0xB8860B)
|
||||
public static let DarkGray = Colour(fromRgb24: 0xA9A9A9)
|
||||
public static let DarkGreen = Colour(fromRgb24: 0x006400)
|
||||
public static let DarkKhaki = Colour(fromRgb24: 0xBDB76B)
|
||||
public static let DarkMagenta = Colour(fromRgb24: 0x8B008B)
|
||||
public static let DarkOliveGreen = Colour(fromRgb24: 0x556B2F)
|
||||
public static let DarkOrange = Colour(fromRgb24: 0xFF8C00)
|
||||
public static let DarkOrchid = Colour(fromRgb24: 0x9932CC)
|
||||
public static let DarkRed = Colour(fromRgb24: 0x8B0000)
|
||||
public static let DarkSalmon = Colour(fromRgb24: 0xE9967A)
|
||||
public static let DarkSeaGreen = Colour(fromRgb24: 0x8FBC8B)
|
||||
public static let DarkSlateBlue = Colour(fromRgb24: 0x483D8B)
|
||||
public static let DarkSlateGray = Colour(fromRgb24: 0x2F4F4F)
|
||||
public static let DarkTurquoise = Colour(fromRgb24: 0x00CED1)
|
||||
public static let DarkViolet = Colour(fromRgb24: 0x9400D3)
|
||||
public static let DeepPink = Colour(fromRgb24: 0xFF1493)
|
||||
public static let DeepSkyBlue = Colour(fromRgb24: 0x00BFFF)
|
||||
public static let DimGray = Colour(fromRgb24: 0x696969)
|
||||
public static let DodgerBlue = Colour(fromRgb24: 0x1E90FF)
|
||||
public static let Firebrick = Colour(fromRgb24: 0xB22222)
|
||||
public static let FloralWhite = Colour(fromRgb24: 0xFFFAF0)
|
||||
public static let ForestGreen = Colour(fromRgb24: 0x228B22)
|
||||
public static let Fuchsia = Colour(fromRgb24: 0xFF00FF)
|
||||
public static let Gainsboro = Colour(fromRgb24: 0xDCDCDC)
|
||||
public static let GhostWhite = Colour(fromRgb24: 0xF8F8FF)
|
||||
public static let Gold = Colour(fromRgb24: 0xFFD700)
|
||||
public static let Goldenrod = Colour(fromRgb24: 0xDAA520)
|
||||
public static let Gray = Colour(fromRgb24: 0x808080)
|
||||
public static let Green = Colour(fromRgb24: 0x008000)
|
||||
public static let GreenYellow = Colour(fromRgb24: 0xADFF2F)
|
||||
public static let Honeydew = Colour(fromRgb24: 0xF0FFF0)
|
||||
public static let HotPink = Colour(fromRgb24: 0xFF69B4)
|
||||
public static let IndianRed = Colour(fromRgb24: 0xCD5C5C)
|
||||
public static let Indigo = Colour(fromRgb24: 0x4B0082)
|
||||
public static let Ivory = Colour(fromRgb24: 0xFFFFF0)
|
||||
public static let Khaki = Colour(fromRgb24: 0xF0E68C)
|
||||
public static let Lavender = Colour(fromRgb24: 0xE6E6FA)
|
||||
public static let LavenderBlush = Colour(fromRgb24: 0xFFF0F5)
|
||||
public static let LawnGreen = Colour(fromRgb24: 0x7CFC00)
|
||||
public static let LemonChiffon = Colour(fromRgb24: 0xFFFACD)
|
||||
public static let LightBlue = Colour(fromRgb24: 0xADD8E6)
|
||||
public static let LightCoral = Colour(fromRgb24: 0xF08080)
|
||||
public static let LightCyan = Colour(fromRgb24: 0xE0FFFF)
|
||||
public static let LightGoldenrodYellow = Colour(fromRgb24: 0xFAFAD2)
|
||||
public static let LightGray = Colour(fromRgb24: 0xD3D3D3)
|
||||
public static let LightGreen = Colour(fromRgb24: 0x90EE90)
|
||||
public static let LightPink = Colour(fromRgb24: 0xFFB6C1)
|
||||
public static let LightSalmon = Colour(fromRgb24: 0xFFA07A)
|
||||
public static let LightSeaGreen = Colour(fromRgb24: 0x20B2AA)
|
||||
public static let LightSkyBlue = Colour(fromRgb24: 0x87CEFA)
|
||||
public static let LightSlateGray = Colour(fromRgb24: 0x778899)
|
||||
public static let LightSteelBlue = Colour(fromRgb24: 0xB0C4DE)
|
||||
public static let LightYellow = Colour(fromRgb24: 0xFFFFE0)
|
||||
public static let Lime = Colour(fromRgb24: 0x00FF00)
|
||||
public static let LimeGreen = Colour(fromRgb24: 0x32CD32)
|
||||
public static let Linen = Colour(fromRgb24: 0xFAF0E6)
|
||||
public static let Magenta = Colour(fromRgb24: 0xFF00FF)
|
||||
public static let Maroon = Colour(fromRgb24: 0x800000)
|
||||
public static let MediumAquamarine = Colour(fromRgb24: 0x66CDAA)
|
||||
public static let MediumBlue = Colour(fromRgb24: 0x0000CD)
|
||||
public static let MediumOrchid = Colour(fromRgb24: 0xBA55D3)
|
||||
public static let MediumPurple = Colour(fromRgb24: 0x9370DB)
|
||||
public static let MediumSeaGreen = Colour(fromRgb24: 0x3CB371)
|
||||
public static let MediumSlateBlue = Colour(fromRgb24: 0x7B68EE)
|
||||
public static let MediumSpringGreen = Colour(fromRgb24: 0x00FA9A)
|
||||
public static let MediumTurquoise = Colour(fromRgb24: 0x48D1CC)
|
||||
public static let MediumVioletRed = Colour(fromRgb24: 0xC71585)
|
||||
public static let MidnightBlue = Colour(fromRgb24: 0x191970)
|
||||
public static let MintCream = Colour(fromRgb24: 0xF5FFFA)
|
||||
public static let MistyRose = Colour(fromRgb24: 0xFFE4E1)
|
||||
public static let Moccasin = Colour(fromRgb24: 0xFFE4B5)
|
||||
public static let MonoGameOrange = Colour(fromRgb24: 0xE73C00)
|
||||
public static let NavajoWhite = Colour(fromRgb24: 0xFFDEAD)
|
||||
public static let Navy = Colour(fromRgb24: 0x000080)
|
||||
public static let OldLace = Colour(fromRgb24: 0xFDF5E6)
|
||||
public static let Olive = Colour(fromRgb24: 0x808000)
|
||||
public static let OliveDrab = Colour(fromRgb24: 0x6B8E23)
|
||||
public static let Orange = Colour(fromRgb24: 0xFFA500)
|
||||
public static let OrangeRed = Colour(fromRgb24: 0xFF4500)
|
||||
public static let Orchid = Colour(fromRgb24: 0xDA70D6)
|
||||
public static let PaleGoldenrod = Colour(fromRgb24: 0xEEE8AA)
|
||||
public static let PaleGreen = Colour(fromRgb24: 0x98FB98)
|
||||
public static let PaleTurquoise = Colour(fromRgb24: 0xAFEEEE)
|
||||
public static let PaleVioletRed = Colour(fromRgb24: 0xDB7093)
|
||||
public static let PapayaWhip = Colour(fromRgb24: 0xFFEFD5)
|
||||
public static let PeachPuff = Colour(fromRgb24: 0xFFDAB9)
|
||||
public static let Peru = Colour(fromRgb24: 0xCD853F)
|
||||
public static let Pink = Colour(fromRgb24: 0xFFC0CB)
|
||||
public static let Plum = Colour(fromRgb24: 0xDDA0DD)
|
||||
public static let PowderBlue = Colour(fromRgb24: 0xB0E0E6)
|
||||
public static let Purple = Colour(fromRgb24: 0x800080)
|
||||
public static let Red = Colour(fromRgb24: 0xFF0000)
|
||||
public static let RosyBrown = Colour(fromRgb24: 0xBC8F8F)
|
||||
public static let RoyalBlue = Colour(fromRgb24: 0x4169E1)
|
||||
public static let SaddleBrown = Colour(fromRgb24: 0x8B4513)
|
||||
public static let Salmon = Colour(fromRgb24: 0xFA8072)
|
||||
public static let SandyBrown = Colour(fromRgb24: 0xF4A460)
|
||||
public static let SeaGreen = Colour(fromRgb24: 0x2E8B57)
|
||||
public static let SeaShell = Colour(fromRgb24: 0xFFF5EE)
|
||||
public static let Sienna = Colour(fromRgb24: 0xA0522D)
|
||||
public static let Silver = Colour(fromRgb24: 0xC0C0C0)
|
||||
public static let SkyBlue = Colour(fromRgb24: 0x87CEEB)
|
||||
public static let SlateBlue = Colour(fromRgb24: 0x6A5ACD)
|
||||
public static let SlateGray = Colour(fromRgb24: 0x708090)
|
||||
public static let Snow = Colour(fromRgb24: 0xFFFAFA)
|
||||
public static let SpringGreen = Colour(fromRgb24: 0x00FF7F)
|
||||
public static let SteelBlue = Colour(fromRgb24: 0x4682B4)
|
||||
public static let Tan = Colour(fromRgb24: 0xD2B48C)
|
||||
public static let Teal = Colour(fromRgb24: 0x008080)
|
||||
public static let Thistle = Colour(fromRgb24: 0xD8BFD8)
|
||||
public static let Tomato = Colour(fromRgb24: 0xFF6347)
|
||||
public static let Turquoise = Colour(fromRgb24: 0x40E0D0)
|
||||
public static let Violet = Colour(fromRgb24: 0xEE82EE)
|
||||
public static let Wheat = Colour(fromRgb24: 0xF5DEB3)
|
||||
public static let White = Colour.white
|
||||
public static let WhiteSmoke = Colour(fromRgb24: 0xF5F5F5)
|
||||
public static let Yellow = Colour(fromRgb24: 0xFFFF00)
|
||||
public static let YellowGreen = Colour(fromRgb24: 0x9ACD32)
|
||||
}
|