Zum Inhalt

native/macos/KuroNativeSaver/Core/RiftScene.swift

// RiftScene — flight down a canyon between two periodic-noise wireframe walls,
// with periodic cosine-eased barrel rolls. Ported from scenes/rift.ts.

import Metal
import simd
import Foundation

final class RiftScene: Scene {
    var camera = Camera()
    var cameraLocked: Bool { true }   // path-locked corridor: no kick overlay (stay in the rift)
    private(set) var items: [DrawItem] = []
    let fogDensity: Float = 0.014

    private let speedPerSec: Float
    private let cAneg, cApos, cBneg, cBpos: DrawItem
    private var cAz: Float = -140
    private var cBz: Float = -420

    // barrel-roll state
    private var nextRollT: Double
    private var rollActive = false
    private var rollStartT: Double = 0
    private var currentRoll: Float = 0
    private var rollRng: LCG

    private static let CHUNK_LEN: Float = 280
    private static let WALL_X: Float = 13

    init(ctx: SceneContext) {
        let device = ctx.device
        speedPerSec = ctx.speed() * 0.22 * 60   // web: *0.22 per frame@60

        camera.position = SIMD3(0, 8, 0)
        camera.rotation = SIMD3(-0.06, 0, 0)

        let segZ = 36, segY = 22
        let K = 2 * Float.pi / RiftScene.CHUNK_LEN

        func wallPositions(_ side: Float) -> [Float] {
            var p = [Float](); p.reserveCapacity((segZ + 1) * (segY + 1) * 3)
            for iy in 0...segY {
                let y0 = 110 - Float(iy) * 10
                for ix in 0...segZ {
                    let z0 = -140 + Float(ix) * (280.0 / Float(segZ))
                    let noise = sin(z0 * 2 * K + side * 12) * 4
                              + cos(z0 * 5 * K + y0 * 0.05) * 2.2
                              + sin(z0 * 12 * K + y0 * 0.03 + side * 7) * 1.4
                    p.append(side * noise); p.append(y0); p.append(z0)
                }
            }
            return p
        }
        let idx = Geo.gridLineIndices(cols: segZ, rows: segY)
        let idxBuf = Geo.buffer(device, idx)
        let vCount = (segZ + 1) * (segY + 1)
        let negBuf = Geo.buffer(device, wallPositions(-1))
        let posBuf = Geo.buffer(device, wallPositions(1))

        func wall(_ buf: MTLBuffer, _ side: Float, _ z: Float) -> DrawItem {
            DrawItem(positions: buf, indices: idxBuf, count: idx.count, vertexCount: vCount,
                     primitive: .line, model: Mathx.translation(SIMD3(side * RiftScene.WALL_X, 8, z)),
                     opacity: 0.55)
        }
        cAneg = wall(negBuf, -1, cAz); cApos = wall(posBuf, 1, cAz)
        cBneg = wall(negBuf, -1, cBz); cBpos = wall(posBuf, 1, cBz)

        // stars
        var rng = ctx.rng
        var sv = [Float](); sv.reserveCapacity(800 * 3)
        for _ in 0..<800 {
            sv.append((rng.nextF() - 0.5) * 220)
            sv.append(30 + rng.nextF() * 130)
            sv.append(-(rng.nextF() * 600 + 60))
        }
        let stars = DrawItem(positions: Geo.buffer(device, sv), indices: nil,
                             count: 800, vertexCount: 800, primitive: .point,
                             opacity: 1.0, isPoint: true, pointSizeWorld: 0.11)

        nextRollT = Double(25 + rng.nextF() * 30)
        rollRng = LCG(seed: (ctx.settings.seed ?? 1) &+ 4242)

        items = [cAneg, cApos, cBneg, cBpos, stars]
    }

    func update(t: Double, dt: Double) {
        let tf = Float(t)
        let spd = speedPerSec * Float(dt)
        cAz += spd; cBz += spd
        if cAz > 148 { cAz -= 560 }
        if cBz > 148 { cBz -= 560 }
        // Gentle evolving serpentine (seam-safe: curve shares the chunk period; slow drift).
        let curveAmp: Float = 5, phase = tf * 0.18
        let kBase = 2 * Float.pi / RiftScene.CHUNK_LEN
        func curveX(_ z: Float) -> Float { curveAmp * sin(z * kBase + phase) }
        let slope = curveAmp * kBase * cos(phase)
        cAneg.model = Mathx.translation(SIMD3(-RiftScene.WALL_X + curveX(cAz), 8, cAz))
        cApos.model = Mathx.translation(SIMD3(RiftScene.WALL_X + curveX(cAz), 8, cAz))
        cBneg.model = Mathx.translation(SIMD3(-RiftScene.WALL_X + curveX(cBz), 8, cBz))
        cBpos.model = Mathx.translation(SIMD3(RiftScene.WALL_X + curveX(cBz), 8, cBz))

        // barrel roll
        if !rollActive && t > nextRollT { rollActive = true; rollStartT = t }
        if rollActive {
            let phase = Float((t - rollStartT) / 1.8)
            if phase >= 1 {
                rollActive = false; currentRoll = 0
                nextRollT = t + Double(30 + rollRng.nextF() * 60)
            } else {
                let eased = 0.5 - 0.5 * cos(phase * .pi)
                currentRoll = eased * .pi * 2
            }
        }

        camera.position = SIMD3(curveX(0) + sin(tf * 0.08) * 2.4, 8 + sin(tf * 0.27) * 0.5, 0)
        camera.rotation = SIMD3(-0.06, cos(tf * 0.08) * 0.03 - slope * 1.2, sin(tf * 0.08) * 0.04 + currentRoll - slope * 2.2)
    }
}