Files
voxelotl-engine/Sources/Voxelotl/Raycast.swift

121 lines
3.3 KiB
Swift
Raw Permalink Normal View History

2024-08-23 21:02:00 +10:00
import simd
2024-08-24 11:10:36 +10:00
public func raycast(
2024-08-25 19:23:47 +10:00
world: World,
2024-08-23 21:02:00 +10:00
origin rayPosition: SIMD3<Float>,
direction: SIMD3<Float>,
maxDistance: Float
) -> Optional<RaycastHit> {
let directionLen = simd_length(direction)
let deltaDistance = SIMD3(direction.indices.map {
direction[$0] != 0.0 ? abs(directionLen / direction[$0]) : Float.greatestFiniteMagnitude
})
2024-08-23 21:02:00 +10:00
var mapPosition = SIMD3<Int>(floor(rayPosition))
var sideDistance: SIMD3<Float> = .zero
var step: SIMD3<Int> = .zero
for i in 0..<3 {
if direction[i] < 0 {
step[i] = -1
sideDistance[i] = (rayPosition[i] - Float(mapPosition[i])) * deltaDistance[i]
} else {
step[i] = 1
sideDistance[i] = (Float(mapPosition[i]) + 1 - rayPosition[i]) * deltaDistance[i]
}
2024-08-23 21:02:00 +10:00
}
// Run digital differential analysis (3DDDA)
2024-08-24 11:10:36 +10:00
var side: RaycastSide
2024-08-23 21:02:00 +10:00
while true {
if sideDistance.x < sideDistance.y {
if sideDistance.x < sideDistance.z {
sideDistance.x += deltaDistance.x
mapPosition.x += step.x
2024-08-24 11:10:36 +10:00
side = step.x > 0 ? .left : .right
2024-08-23 21:02:00 +10:00
} else {
sideDistance.z += deltaDistance.z
mapPosition.z += step.z
2024-08-24 11:10:36 +10:00
side = step.z > 0 ? .front : .back
2024-08-23 21:02:00 +10:00
}
} else {
if sideDistance.y < sideDistance.z {
sideDistance.y += deltaDistance.y
mapPosition.y += step.y
2024-08-24 11:10:36 +10:00
side = step.y > 0 ? .down : .up
2024-08-23 21:02:00 +10:00
} else {
sideDistance.z += deltaDistance.z
mapPosition.z += step.z
2024-08-24 11:10:36 +10:00
side = step.z > 0 ? .front : .back
2024-08-23 21:02:00 +10:00
}
}
2024-08-24 11:10:36 +10:00
// Compute distance
let distance: Float = if side.isX {
abs(Float(mapPosition.x) - rayPosition.x + Float(1 - step.x) * 0.5) * deltaDistance.x
2024-08-24 11:10:36 +10:00
} else if side.isVertical {
abs(Float(mapPosition.y) - rayPosition.y + Float(1 - step.y) * 0.5) * deltaDistance.y
2024-08-23 21:02:00 +10:00
} else {
abs(Float(mapPosition.z) - rayPosition.z + Float(1 - step.z) * 0.5) * deltaDistance.z
2024-08-23 21:02:00 +10:00
}
2024-08-24 11:10:36 +10:00
// Bail out if we've exeeded the max raycast distance
2024-08-23 21:02:00 +10:00
if distance > maxDistance {
return nil
}
2024-08-24 11:10:36 +10:00
// Return a result if we hit something solid
2024-08-25 19:23:47 +10:00
if world.getBlock(at: mapPosition).type != .air {
2024-08-23 21:02:00 +10:00
return .init(
position: rayPosition + direction / directionLen * distance,
2024-08-23 21:02:00 +10:00
distance: distance,
map: mapPosition,
2024-08-24 11:10:36 +10:00
side: side)
}
}
}
public struct RaycastHit {
let position: SIMD3<Float>
let distance: Float
let map: SIMD3<Int>
let side: RaycastSide
}
public enum RaycastSide {
case left, right
case down, up
case back, front
}
public extension SIMD3 where Scalar == Int {
func offset(by side: RaycastSide) -> Self {
let ofs: Self = switch side {
2024-09-01 21:16:05 +10:00
case .right: .right
case .left: .left
case .up: .up
case .down: .down
case .back: .back
case .front: .forward
2024-08-23 21:02:00 +10:00
}
2024-08-24 11:10:36 +10:00
return self &+ ofs
2024-08-23 21:02:00 +10:00
}
}
2024-08-24 11:10:36 +10:00
public extension RaycastSide {
func normal<T: FloatingPoint>() -> SIMD3<T> {
switch self {
case .left: .left
case .right: .right
case .down: .down
case .up: .up
case .back: .back
case .front: .forward
}
}
@inline(__always) var isX: Bool { self == .left || self == .right }
@inline(__always) var isZ: Bool { self == .back || self == .front }
@inline(__always) var isHorizontal: Bool { self.isX || self.isZ }
@inline(__always) var isVertical: Bool { self == .up || self == .down }
}