diff --git a/Terrain.xcodeproj/project.pbxproj b/Terrain.xcodeproj/project.pbxproj index b056067..5ee5f6d 100644 --- a/Terrain.xcodeproj/project.pbxproj +++ b/Terrain.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ C018AD33219518080094BE3C /* Terrain2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C018AD32219518080094BE3C /* Terrain2Tests.swift */; }; + C018AD3B219518480094BE3C /* AlgorithmsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C018AD3A219518480094BE3C /* AlgorithmsTests.swift */; }; C019C8512191CE7100EAD5BB /* Uniforms.m in Sources */ = {isa = PBXBuildFile; fileRef = C019C8502191CE7100EAD5BB /* Uniforms.m */; }; C08C58A0218F46F000EAFC2D /* Algorithms.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C589F218F46F000EAFC2D /* Algorithms.swift */; }; C08C58A2218F474E00EAFC2D /* TerrainAlgorithms.metal in Sources */ = {isa = PBXBuildFile; fileRef = C08C58A1218F474E00EAFC2D /* TerrainAlgorithms.metal */; }; @@ -44,6 +45,7 @@ C018AD30219518080094BE3C /* Terrain2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Terrain2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C018AD32219518080094BE3C /* Terrain2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Terrain2Tests.swift; sourceTree = ""; }; C018AD34219518080094BE3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C018AD3A219518480094BE3C /* AlgorithmsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmsTests.swift; sourceTree = ""; }; C019C8502191CE7100EAD5BB /* Uniforms.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Uniforms.m; sourceTree = ""; }; C08C589F218F46F000EAFC2D /* Algorithms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Algorithms.swift; sourceTree = ""; }; C08C58A1218F474E00EAFC2D /* TerrainAlgorithms.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = TerrainAlgorithms.metal; sourceTree = ""; }; @@ -104,6 +106,7 @@ isa = PBXGroup; children = ( C018AD32219518080094BE3C /* Terrain2Tests.swift */, + C018AD3A219518480094BE3C /* AlgorithmsTests.swift */, C018AD34219518080094BE3C /* Info.plist */, ); path = Terrain2Tests; @@ -307,6 +310,7 @@ buildActionMask = 2147483647; files = ( C018AD33219518080094BE3C /* Terrain2Tests.swift in Sources */, + C018AD3B219518480094BE3C /* AlgorithmsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -372,9 +376,10 @@ isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 78372RE6B4; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Terrain2Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -383,6 +388,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = me.erynwells.Terrain2Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Terrain2.app/Contents/MacOS/Terrain2"; }; @@ -392,9 +398,10 @@ isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 78372RE6B4; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Terrain2Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -403,6 +410,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = me.erynwells.Terrain2Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Terrain2.app/Contents/MacOS/Terrain2"; }; @@ -566,9 +574,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Terrain2/Terrain2.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 78372RE6B4; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Terrain2/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -576,6 +585,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = me.erynwells.Terrain2; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = Terrain2/ShaderTypes.h; SWIFT_VERSION = 4.2; }; @@ -586,9 +596,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Terrain2/Terrain2.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 78372RE6B4; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Terrain2/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -596,6 +607,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = me.erynwells.Terrain2; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = Terrain2/ShaderTypes.h; SWIFT_VERSION = 4.2; }; diff --git a/Terrain2/Algorithms.swift b/Terrain2/Algorithms.swift index 7bf1321..fd8f4c3 100644 --- a/Terrain2/Algorithms.swift +++ b/Terrain2/Algorithms.swift @@ -23,7 +23,10 @@ protocol Algorithm { } class Kernel { - static let textureSize = MTLSize(width: 512, height: 512, depth: 1) + + class var textureSize: MTLSize { + return MTLSize(width: 512, height: 512, depth: 1) + } class func buildTexture(device: MTLDevice, size: MTLSize) -> MTLTexture? { let desc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r32Float, width: size.width, height: size.height, mipmapped: false) @@ -51,7 +54,7 @@ class Kernel { // Create our input and output textures var textures = [MTLTexture]() for i in 0..<2 { - guard let tex = Kernel.buildTexture(device: device, size: Kernel.textureSize) else { + guard let tex = Kernel.buildTexture(device: device, size: type(of: self).textureSize) else { print("Couldn't create heights texture i=\(i)") throw KernelError.textureCreationFailed } @@ -67,7 +70,7 @@ class Kernel { encoder.setTexture(textures[textureIndexes.in], index: textureIndexes.in) encoder.setTexture(textures[textureIndexes.out], index: textureIndexes.out) encoder.setBuffer(uniformBuffer, offset: 0, index: 0) - encoder.dispatchThreads(Kernel.textureSize, threadsPerThreadgroup: MTLSize(width: 8, height: 8, depth: 1)) + encoder.dispatchThreads(type(of: self).textureSize, threadsPerThreadgroup: MTLSize(width: 8, height: 8, depth: 1)) } } @@ -121,9 +124,172 @@ class RandomAlgorithm: Kernel, Algorithm { /// Implementation of the Diamond-Squares algorithm. /// - https://en.wikipedia.org/wiki/Diamond-square_algorithm -//class DiamondSquareAlgorithm: Algorithm { -// static let name = "Diamond-Square" -//} +public class DiamondSquareAlgorithm: Algorithm { + public struct Box { + public typealias Point = (x: Int, y: Int) + public typealias Size = (w: Int, h: Int) + + let origin: Point + let size: Size + + public init(origin o: Point, size s: Size) { + origin = o + size = s + } + + public var corners: [Point] { + return [northwest, southwest, northeast, northwest] + } + + public var sideMidpoints: [Point] { + return [north, west, south, east] + } + + public var north: Point { + return (x: origin.x + (size.w / 2 + 1), y: origin.y) + } + + public var west: Point { + return (x: origin.x, y: origin.y + (size.h / 2 + 1)) + } + + public var south: Point { + return (x: origin.x + (size.w / 2 + 1), y: origin.y + size.h) + } + + public var east: Point { + return (x: origin.x + size.w, y: origin.y + (size.h / 2 + 1)) + } + + public var northwest: Point { + return origin + } + + public var southwest: Point { + return (x: origin.x, y: origin.y + size.h) + } + + public var northeast: Point { + return (x: origin.x + size.w, y: origin.y) + } + + public var southeast: Point { + return (x: origin.x + size.w, y: origin.y + size.h) + } + + public var midpoint: Point { + return (x: origin.x + (size.w / 2 + 1), y: origin.y + (size.h / 2 + 1)) + } + } + + let name = "Diamond-Square" + + class var textureSize: MTLSize { + // Needs to 2n + 1 on each side. + return MTLSize(width: 513, height: 513, depth: 1) + } + + let texture: MTLTexture + let textureSemaphore = DispatchSemaphore(value: 1) + + init?(device: MTLDevice) { + let size = DiamondSquareAlgorithm.textureSize + let desc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r32Float, width: size.width, height: size.height, mipmapped: false) + desc.usage = [.shaderRead, .shaderWrite] + desc.resourceOptions = .storageModeShared + guard let tex = device.makeTexture(descriptor: desc) else { + print("Couldn't create texture for Diamond-Squares algorithm.") + return nil + } + texture = tex + } + + func render() { + let size = DiamondSquareAlgorithm.textureSize + + func ptToIndex(_ pt: Box.Point) -> Int { + return pt.y * size.width + pt.x + } + + var heightMap = [Float](repeating: 0, count: size.width * size.height) + var queue: [Box] = [Box(origin: (0, 0), size: (size.width, size.height))] + + // 0. Set the corners to initial values if they haven't been set yet. + for p in queue.first!.corners { + let idx = ptToIndex(p) + if heightMap[idx] == 0.0 { + heightMap[idx] = Float.random(in: 0...1) + } + } + + while queue.count > 0 { + let box = queue.removeFirst() + let halfSize = (w: box.size.w / 2 + 1, h: box.size.h / 2 + 1) + + // 1. Diamond. Average the corners, add a random value. Set the midpoint. + let midpoint = box.midpoint + let cornerAverage = Float.random(in: 0...1) + 0.25 * box.corners.reduce(0.0) { (acc, pt) -> Float in + let index = ptToIndex(pt) + let value = heightMap[index] + return acc + value + } + let midptIdx = ptToIndex(midpoint) + heightMap[midptIdx] = cornerAverage + + // 2. Square. Find the midpoints of the sides of this box. These four points are the origins of the new subdivided boxes. + for p in box.sideMidpoints { + // Find our diamond's corners, wrapping around the grid if needed. + let diamondCorners = [ + (x: p.x, y: p.y - halfSize.h), // North + (x: p.x - halfSize.w, y: p.y), // West + (x: p.x, y: (p.y + halfSize.h) % size.height), // South + (x: (p.x + halfSize.w) % size.width, y: p.y), // West + ].map { (p: Box.Point) -> Box.Point in + if p.x < 0 { + return (x: p.x + size.width, y: p.y) + } else if p.y < 0 { + return (x: p.x, y: p.y + size.height) + } else { + return p + } + } + + let idx = ptToIndex(p) + let value = Float.random(in: 0...1) + 0.25 * diamondCorners.reduce(0) { (acc, pt) -> Float in + let idx = ptToIndex(pt) + let value = heightMap[idx] + return acc + value + } + heightMap[idx] = value + } + + // 3. Base case for this recursion is boxes of size 1. Subdivide this box into 4 and push them onto the queue. + if box.size.w > 1 || box.size.h > 1 { + let newSize = (w: midpoint.x - box.origin.x, h: midpoint.y - box.origin.y) + let newBoxes = [Box(origin: box.origin, size: newSize), + Box(origin: midpoint, size: newSize), + Box(origin: (box.origin.x, box.origin.1 + newSize.1), size: newSize), + Box(origin: (box.origin.x + newSize.w, box.origin.y + newSize.h), size: newSize)] + queue.append(contentsOf: newBoxes) + } + } + + let region = MTLRegion(origin: MTLOrigin(), size: size) + texture.replace(region: region, mipmapLevel: 0, withBytes: heightMap, bytesPerRow: MemoryLayout.stride * size.width) + } + + // MARK: Algorithm + + var outTexture: MTLTexture { + return texture + } + + func encode(in encoder: MTLComputeCommandEncoder) { + } + + func updateUniforms() { + } +} /// Implementation of the Circles algorithm. //class CirclesAlgorithm: Algorithm { diff --git a/Terrain2Tests/AlgorithmsTests.swift b/Terrain2Tests/AlgorithmsTests.swift new file mode 100644 index 0000000..08936b2 --- /dev/null +++ b/Terrain2Tests/AlgorithmsTests.swift @@ -0,0 +1,52 @@ +// +// AlgorithmsTests.swift +// Terrain2Tests +// +// Created by Eryn Wells on 11/8/18. +// Copyright © 2018 Eryn Wells. All rights reserved. +// + +import XCTest +import Terrain2 + +class DiamondSquareBoxTests: XCTestCase { + func testNorthwest() { + let box = DiamondSquareAlgorithm.Box(origin: DiamondSquareAlgorithm.Box.Point(x: 3, y: 4), + size: DiamondSquareAlgorithm.Box.Size(w: 5, h: 5)) + let pt = box.northwest + XCTAssertEqual(pt.x, 3) + XCTAssertEqual(pt.y, 4) + } + + func testNortheast() { + let box = DiamondSquareAlgorithm.Box(origin: DiamondSquareAlgorithm.Box.Point(x: 3, y: 4), + size: DiamondSquareAlgorithm.Box.Size(w: 5, h: 5)) + let pt = box.northeast + XCTAssertEqual(pt.x, 8) + XCTAssertEqual(pt.y, 4) + } + + func testSouthwest() { + let box = DiamondSquareAlgorithm.Box(origin: DiamondSquareAlgorithm.Box.Point(x: 3, y: 4), + size: DiamondSquareAlgorithm.Box.Size(w: 5, h: 5)) + let pt = box.southwest + XCTAssertEqual(pt.x, 3) + XCTAssertEqual(pt.y, 9) + } + + func testSoutheast() { + let box = DiamondSquareAlgorithm.Box(origin: DiamondSquareAlgorithm.Box.Point(x: 3, y: 4), + size: DiamondSquareAlgorithm.Box.Size(w: 5, h: 5)) + let pt = box.southeast + XCTAssertEqual(pt.x, 8) + XCTAssertEqual(pt.y, 9) + } + + func testMidpoint() { + let box = DiamondSquareAlgorithm.Box(origin: DiamondSquareAlgorithm.Box.Point(x: 3, y: 4), + size: DiamondSquareAlgorithm.Box.Size(w: 5, h: 5)) + let midpoint = box.midpoint + XCTAssertEqual(midpoint.x, 6) + XCTAssertEqual(midpoint.y, 7) + } +}