Skip to content

Instantly share code, notes, and snippets.

@dduan
Last active November 26, 2020 08:55
Show Gist options
  • Select an option

  • Save dduan/ed77683dcc9b1a52f533d17f266aa769 to your computer and use it in GitHub Desktop.

Select an option

Save dduan/ed77683dcc9b1a52f533d17f266aa769 to your computer and use it in GitHub Desktop.

Revisions

  1. dduan revised this gist Nov 22, 2020. 1 changed file with 5 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions cube.swift
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,8 @@
    /// A rotating 3-D cube in terminal
    /// Only works on macOS
    /// Run `swift cube.swift` in a terminal application to run it.
    /// For controlling the cube, see comments for `Key` in code.

    import Darwin

    enum RawModeError: Error {
  2. dduan revised this gist Nov 22, 2020. 1 changed file with 0 additions and 10 deletions.
    10 changes: 0 additions & 10 deletions cube.swift
    Original file line number Diff line number Diff line change
    @@ -169,19 +169,9 @@ struct Vec3 {
    }
    }

    extension Vec3: CustomStringConvertible {
    var description: String {
    "(\(x),\(y),\(z))"
    }
    }

    struct Cube {
    var size: Float = 1
    var scanFrequency: Float = 0.1
    var position: Vec3 = .init(x: 0, y: 0, z: 0)
    var maxZ: Float {
    sqrt(3) * size
    }

    // (texture for the surface, vector perpendicular to the surfaces, points on the surface)
    var surfaces: [(Character, Vec3, [Vec3])] {
  3. dduan revised this gist Nov 22, 2020. 1 changed file with 40 additions and 40 deletions.
    80 changes: 40 additions & 40 deletions cube.swift
    Original file line number Diff line number Diff line change
    @@ -158,55 +158,55 @@ class Game {
    }

    struct Vec3 {
    let x, y, z: Float
    let x, y, z: Float

    static func + (lhs: Self, rhs: Self) -> Self {
    .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
    }
    static func + (lhs: Self, rhs: Self) -> Self {
    .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
    }

    static func * (lhs: Self, rhs: Float) -> Self {
    .init(x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs)
    }
    static func * (lhs: Self, rhs: Float) -> Self {
    .init(x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs)
    }
    }

    extension Vec3: CustomStringConvertible {
    var description: String {
    "(\(x),\(y),\(z))"
    }
    var description: String {
    "(\(x),\(y),\(z))"
    }
    }

    struct Cube {
    var size: Float = 1
    var scanFrequency: Float = 0.1
    var position: Vec3 = .init(x: 0, y: 0, z: 0)
    var maxZ: Float {
    sqrt(3) * size
    }

    // (texture for the surface, vector perpendicular to the surfaces, points on the surface)
    var surfaces: [(Character, Vec3, [Vec3])] {
    [(Character, Vec3, Vec3, Vec3, Vec3)]([
    ("", .init(x: 0, y: 0, z: -1), .init(x: -0.5, y: -0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: 0, y: 1, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 0, z: 1)),
    ("", .init(x: 0, y: 0, z: 1), .init(x: -0.5, y: 0.5, z: 0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: -1, z: 0)),
    ("", .init(x: 0, y: -1, z: 0), .init(x: -0.5, y: -0.5, z: 0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 0, z: -1)),
    ("", .init(x: 1, y: 0, z: 0), .init(x: 0.5, y: -0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: -1, y: 0, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: -1, z: 0)),
    ])
    .map { texture, perp, start, d1, d2 in
    (
    texture,
    perp,
    stride(from: 0, to: 1, by: scanFrequency)
    .flatMap { d2Offset in
    stride(from: 0, to: 1, by: scanFrequency)
    .map { d1Offset in
    (start + d1 * d1Offset + d2 * d2Offset) * size
    }
    }
    )
    var size: Float = 1
    var scanFrequency: Float = 0.1
    var position: Vec3 = .init(x: 0, y: 0, z: 0)
    var maxZ: Float {
    sqrt(3) * size
    }

    // (texture for the surface, vector perpendicular to the surfaces, points on the surface)
    var surfaces: [(Character, Vec3, [Vec3])] {
    [(Character, Vec3, Vec3, Vec3, Vec3)]([
    ("", .init(x: 0, y: 0, z: -1), .init(x: -0.5, y: -0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: 0, y: 1, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 0, z: 1)),
    ("", .init(x: 0, y: 0, z: 1), .init(x: -0.5, y: 0.5, z: 0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: -1, z: 0)),
    ("", .init(x: 0, y: -1, z: 0), .init(x: -0.5, y: -0.5, z: 0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 0, z: -1)),
    ("", .init(x: 1, y: 0, z: 0), .init(x: 0.5, y: -0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: -1, y: 0, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: -1, z: 0)),
    ])
    .map { texture, perp, start, d1, d2 in
    (
    texture,
    perp,
    stride(from: 0, to: 1, by: scanFrequency)
    .flatMap { d2Offset in
    stride(from: 0, to: 1, by: scanFrequency)
    .map { d1Offset in
    (start + d1 * d1Offset + d2 * d2Offset) * size
    }
    }
    )
    }
    }
    }
    }

    final class CubeGame: Game {
  4. dduan revised this gist Nov 22, 2020. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions cube.swift
    Original file line number Diff line number Diff line change
    @@ -184,7 +184,7 @@ struct Cube {
    }

    // (texture for the surface, vector perpendicular to the surfaces, points on the surface)
    var points: [(Character, Vec3, [Vec3])] {
    var surfaces: [(Character, Vec3, [Vec3])] {
    [(Character, Vec3, Vec3, Vec3, Vec3)]([
    ("", .init(x: 0, y: 0, z: -1), .init(x: -0.5, y: -0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: 0, y: 1, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 0, z: 1)),
    @@ -267,17 +267,17 @@ final class CubeGame: Game {

    override func render() {
    graphic.clear()
    for (c, perp, surface) in cube.points {
    for (texture, perp, points) in cube.surfaces {
    // if an vector perpendicular to a surface has negative dot product with one such vector that points towards the view point, it is perpendicular to a hidden surface.
    let rotatedPerp = rotate(vec: perp)
    if -rotatedPerp.z <= 0 {
    continue
    }

    for point in surface {
    for point in points {
    let rotated = rotate(vec: point)
    // coordinates need to be shifted to the middle of the view port
    graphic[Int(rotated.x.rounded(.down)) + graphic.width / 2, Int(rotated.y.rounded(.down)) + graphic.height / 2 + 1] = c
    graphic[Int(rotated.x.rounded(.down)) + graphic.width / 2, Int(rotated.y.rounded(.down)) + graphic.height / 2 + 1] = texture
    }
    }
    }
  5. dduan revised this gist Nov 22, 2020. 1 changed file with 10 additions and 6 deletions.
    16 changes: 10 additions & 6 deletions cube.swift
    Original file line number Diff line number Diff line change
    @@ -183,6 +183,7 @@ struct Cube {
    sqrt(3) * size
    }

    // (texture for the surface, vector perpendicular to the surfaces, points on the surface)
    var points: [(Character, Vec3, [Vec3])] {
    [(Character, Vec3, Vec3, Vec3, Vec3)]([
    ("", .init(x: 0, y: 0, z: -1), .init(x: -0.5, y: -0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 1, z: 0)),
    @@ -192,10 +193,10 @@ struct Cube {
    ("", .init(x: 1, y: 0, z: 0), .init(x: 0.5, y: -0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: -1, y: 0, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: -1, z: 0)),
    ])
    .map { c, orth, start, d1, d2 in
    .map { texture, perp, start, d1, d2 in
    (
    c,
    orth,
    texture,
    perp,
    stride(from: 0, to: 1, by: scanFrequency)
    .flatMap { d2Offset in
    stride(from: 0, to: 1, by: scanFrequency)
    @@ -246,6 +247,7 @@ final class CubeGame: Game {
    }
    }

    // Apply standard rotation matrix
    func rotate(vec: Vec3) -> Vec3 {
    let x = vec.x
    let y = vec.y
    @@ -265,14 +267,16 @@ final class CubeGame: Game {

    override func render() {
    graphic.clear()
    for (c, orth, surface) in cube.points {
    let rotatedOrth = rotate(vec: orth)
    if -rotatedOrth.z <= 0 {
    for (c, perp, surface) in cube.points {
    // if an vector perpendicular to a surface has negative dot product with one such vector that points towards the view point, it is perpendicular to a hidden surface.
    let rotatedPerp = rotate(vec: perp)
    if -rotatedPerp.z <= 0 {
    continue
    }

    for point in surface {
    let rotated = rotate(vec: point)
    // coordinates need to be shifted to the middle of the view port
    graphic[Int(rotated.x.rounded(.down)) + graphic.width / 2, Int(rotated.y.rounded(.down)) + graphic.height / 2 + 1] = c
    }
    }
  6. dduan created this gist Nov 22, 2020.
    282 changes: 282 additions & 0 deletions cube.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,282 @@
    import Darwin

    enum RawModeError: Error {
    case notATerminal
    case failedToGetTerminalSetting
    case failedToSetTerminalSetting
    }

    func runInRawMode(_ task: @escaping () throws -> Void) throws {
    var originalTermSetting = termios()
    guard isatty(STDIN_FILENO) != 0 else {
    throw RawModeError.notATerminal
    }

    guard tcgetattr(STDIN_FILENO, &originalTermSetting) >= 0 else {
    throw RawModeError.failedToGetTerminalSetting
    }


    var raw = originalTermSetting
    raw.c_iflag &= ~(UInt(BRKINT) | UInt(ICRNL) | UInt(INPCK) | UInt(ISTRIP) | UInt(IXON))
    raw.c_oflag &= ~(UInt(OPOST))
    raw.c_cflag |= UInt(CS8)
    raw.c_lflag &= ~(UInt(ECHO) | UInt(ICANON) | UInt(IEXTEN) | UInt(ISIG))
    raw.c_cc.16 = 0
    raw.c_cc.17 = 1

    guard tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) >= 0 else {
    throw RawModeError.failedToSetTerminalSetting
    }

    defer {
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &originalTermSetting)
    print("\u{1b}[?25h", terminator: "")
    print("\u{1b}[0;0H")
    }

    print("\u{1b}[2J")
    print("\u{1b}[?25l")

    try task()
    }

    enum Key {
    static let q = Character("q").asciiValue! // quit
    static let d = Character("d").asciiValue! // show debug info
    static let j = Character("j").asciiValue! // increase yaw
    static let k = Character("k").asciiValue! // decrease yaw
    static let x = Character("x").asciiValue! // increase pitch
    static let b = Character("b").asciiValue! // decrease pitch
    static let m = Character("m").asciiValue! // increase roll
    static let w = Character("w").asciiValue! // decrease roll
    static let r = Character("r").asciiValue! // reset
    }

    final class Graphic {
    var buffer: [Character]
    let width: Int
    let height: Int
    var clearedBuffer: [Character]

    init(width: Int, height: Int) {
    self.width = width
    self.height = height
    clearedBuffer = .init(repeating: " ", count: width * height)
    buffer = clearedBuffer
    }

    subscript(x: Int, y: Int) -> Character {
    get {
    let index = y * width + x
    assert(index < buffer.count)
    return buffer[index]
    }

    set {
    let index = y * width + x
    guard index >= 0 && index < buffer.count else { return }
    buffer[index] = newValue
    }
    }

    func clear() {
    buffer = clearedBuffer
    }
    }

    extension timeval {
    func microseconds(since other: timeval) -> Double {
    Double(self.tv_usec - other.tv_usec) / 1000 + Double(self.tv_sec - other.tv_sec) * 1000
    }
    }

    class Game {
    let graphic: Graphic
    let fps: Double
    let msPerRender: Double
    var lastRender = timeval()
    var showDebugInfo = false
    var loopCount = 0
    var exit = false

    var now: timeval {
    var result = timeval()
    gettimeofday(&result, nil)
    return result
    }

    init(graphicWidth: Int, graphicHeight: Int, fps: Double = 60) {
    self.graphic = .init(width: graphicWidth, height: graphicHeight)
    self.fps = fps
    self.msPerRender = 1 / fps
    }

    func positionAt(x: Int, y: Int) {
    print("\u{1b}[\(y);\(x)H", terminator: "")
    }

    func run() throws {
    try runInRawMode { [weak self] in
    guard let self = self else { return }
    while !self.exit {
    var input: UInt8 = 0
    read(STDIN_FILENO, &input, 1)
    self.handle(input: input)
    self.update()
    let now = self.now
    let msSince = now.microseconds(since: self.lastRender)
    if msSince >= self.msPerRender {
    self.lastRender = now
    self.render()
    self.positionAt(x: 0, y: 0)
    for line in 0 ..< self.graphic.height {
    let start = line * self.graphic.width
    let end = start + self.graphic.width
    self.positionAt(x: 0, y: line)
    print(String(self.graphic.buffer[start ..< end]))
    }

    if self.showDebugInfo {
    self.positionAt(x: 1, y: 1)
    let lps = Int(Double(self.loopCount) / (1 / msSince))
    print("w: \(self.graphic.width) h: \(self.graphic.height) lps: \(lps)")
    self.loopCount = 0
    }
    }

    self.loopCount += 1
    }
    }

    print("\u{1b}[2J")
    }

    open func handle(input: UInt8) {}
    open func update() {}
    open func render() {}
    }

    struct Vec3 {
    let x, y, z: Float

    static func + (lhs: Self, rhs: Self) -> Self {
    .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
    }

    static func * (lhs: Self, rhs: Float) -> Self {
    .init(x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs)
    }
    }

    extension Vec3: CustomStringConvertible {
    var description: String {
    "(\(x),\(y),\(z))"
    }
    }

    struct Cube {
    var size: Float = 1
    var scanFrequency: Float = 0.1
    var position: Vec3 = .init(x: 0, y: 0, z: 0)
    var maxZ: Float {
    sqrt(3) * size
    }

    var points: [(Character, Vec3, [Vec3])] {
    [(Character, Vec3, Vec3, Vec3, Vec3)]([
    ("", .init(x: 0, y: 0, z: -1), .init(x: -0.5, y: -0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: 0, y: 1, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 0, z: 1)),
    ("", .init(x: 0, y: 0, z: 1), .init(x: -0.5, y: 0.5, z: 0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: -1, z: 0)),
    ("", .init(x: 0, y: -1, z: 0), .init(x: -0.5, y: -0.5, z: 0.5), .init(x: 1, y: 0, z: 0), .init(x: 0, y: 0, z: -1)),
    ("", .init(x: 1, y: 0, z: 0), .init(x: 0.5, y: -0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: 1, z: 0)),
    ("", .init(x: -1, y: 0, z: 0), .init(x: -0.5, y: 0.5, z: -0.5), .init(x: 0, y: 0, z: 1), .init(x: 0, y: -1, z: 0)),
    ])
    .map { c, orth, start, d1, d2 in
    (
    c,
    orth,
    stride(from: 0, to: 1, by: scanFrequency)
    .flatMap { d2Offset in
    stride(from: 0, to: 1, by: scanFrequency)
    .map { d1Offset in
    (start + d1 * d1Offset + d2 * d2Offset) * size
    }
    }
    )
    }
    }
    }

    final class CubeGame: Game {
    var cube: Cube
    var yaw: Float = 0
    var pitch: Float = 0
    var roll: Float = 0

    init() {
    cube = .init(size: 18, scanFrequency: 0.04)
    super.init(graphicWidth: 50, graphicHeight: 50)
    }

    override func handle(input: UInt8) {
    switch input {
    case Key.q:
    self.exit = true
    case Key.d:
    self.showDebugInfo.toggle()
    case Key.j:
    self.yaw += 0.1
    case Key.k:
    self.yaw -= 0.1
    case Key.x:
    self.roll += 0.1
    case Key.b:
    self.roll -= 0.1
    case Key.m:
    self.pitch += 0.1
    case Key.w:
    self.pitch -= 0.1
    case Key.r:
    self.pitch = 0
    self.yaw = 0
    self.roll = 0
    default:
    break
    }
    }

    func rotate(vec: Vec3) -> Vec3 {
    let x = vec.x
    let y = vec.y
    let z = vec.z

    let cosYaw = cos(yaw)
    let sinYaw = sin(yaw)
    let cosPitch = cos(pitch)
    let sinPitch = sin(pitch)
    let cosRoll = cos(roll)
    let sinRoll = sin(roll)
    let rotatedX = cosYaw * cosPitch * x + (cosYaw * sinPitch * sinRoll - sinYaw * cosRoll) * y + (cosYaw * sinPitch * cosRoll + sinYaw * sinRoll) * z
    let rotatedY = sinYaw * cosPitch * x + (sinYaw * sinPitch * sinRoll + cosYaw * cosRoll) * y + (sinYaw * sinPitch * cosRoll - cosYaw * sinRoll) * z
    let rotatedZ = -sinPitch * x + cosPitch * sinRoll * y + cosPitch * cosRoll * z
    return .init(x: rotatedX, y: rotatedY, z: rotatedZ)
    }

    override func render() {
    graphic.clear()
    for (c, orth, surface) in cube.points {
    let rotatedOrth = rotate(vec: orth)
    if -rotatedOrth.z <= 0 {
    continue
    }

    for point in surface {
    let rotated = rotate(vec: point)
    graphic[Int(rotated.x.rounded(.down)) + graphic.width / 2, Int(rotated.y.rounded(.down)) + graphic.height / 2 + 1] = c
    }
    }
    }
    }

    try CubeGame().run()