Files
Gas-Tank-AR-App/GasTankAR/Views/TankAR/TankPitFactory.swift
2026-09-02 20:54:36 +02:00

212 lines
8.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TankPitFactory.swift
// GasTankAR
//
// Created by Dennis Nemec on 28.08.26.
//
import RealityKit
import UIKit
/// Baut die „ausgehobene Grube" für einen unterirdischen Tank als **Zylinder**: Der Boden wird
/// mit einer kreisrunden Occlusion-Maske „aufgerissen" und darunter ein runder Schacht
/// ausgehoben (sichtbarer Wandmantel + Sohle), in dem der Tank steht.
///
/// Aufbau der zurückgegebenen Entity (lokaler Ursprung = Bodenkante, y = 0):
/// - Wandmantel + Sohle reichen von y = 0 bis y = -`depth`.
/// - Eine kreisrunde Occlusion-Maske in der Bodenebene verdeckt alles Vergrabene außerhalb
/// der Öffnung, sodass der Eindruck eines echten Lochs im Boden entsteht.
@MainActor
enum TankPitFactory {
/// Anzahl der Segmente pro Kreis (höher = runder).
private static let segments = 48
/// Radius der kreisförmigen Boden-Occlusion-Maske (muss die Öffnung deutlich überragen).
private static let maskOuterRadius: Float = 20
/// Die Maske liegt minimal unter der Bodenkante, damit sie nicht mit bodennahen
/// Overlays (z. B. den Sicherheitsabstands-Ringen bei y = 0) um die Tiefe konkurriert.
private static let maskYOffset: Float = -0.005
/// Breite der hervorgehobenen Rand-Ringe.
private static let rimBand: Float = 0.03
/// Farbe des Wandmantels (heller, damit sich die Grube klar vom Boden abhebt).
private static let wallColor = UIColor(red: 0.62, green: 0.47, blue: 0.31, alpha: 1)
/// Farbe der Grubensohle (dunkler als die Wände – erzeugt Tiefeneindruck).
private static let floorColor = UIColor(red: 0.24, green: 0.18, blue: 0.12, alpha: 1)
/// Farbe der hervorgehobenen Rand-Ringe.
private static let rimColor = UIColor.systemYellow
/// Erzeugt die zylindrische Grube.
/// - Parameters:
/// - radius: Radius der Grube (x/z) in Metern.
/// - depth: Tiefe der Grube (y) in Metern.
/// - center: horizontaler Mittelpunkt relativ zum Container (y wird ignoriert).
static func makePit(radius: Float, depth: Float, center: SIMD3<Float>) -> Entity {
let pit = Entity()
pit.name = "TankPit"
pit.position = SIMD3(center.x, 0, center.z)
// Sichtbarer Wandmantel mit Erd-/Dreck-Textur …
pit.addChild(wallModel(radius: radius, depth: depth))
// … runde Sohle (dunkler) …
pit.addChild(model(mesh: floorMesh(radius: radius, depth: depth), color: floorColor))
// … hervorgehobene Rand-Ringe oben und unten …
pit.addChild(model(mesh: ringMesh(radius: radius, y: 0), color: rimColor))
pit.addChild(model(mesh: ringMesh(radius: radius, y: -depth), color: rimColor))
// … und die kreisrunde Occlusion-Maske mit Loch über der Öffnung.
let mask = ModelEntity(
mesh: maskMesh(holeRadius: radius, outerRadius: maskOuterRadius),
materials: [OcclusionMaterial()]
)
mask.position.y = maskYOffset
pit.addChild(mask)
return pit
}
// MARK: - Bausteine
private static func model(mesh: MeshResource, color: UIColor) -> ModelEntity {
var material = UnlitMaterial(color: color)
material.faceCulling = .none // beidseitig sichtbar (Blick von außen und in die Grube)
return ModelEntity(mesh: mesh, materials: [material])
}
/// Wandmantel mit gekachelter Erd-Textur (Fallback: einfarbig).
private static func wallModel(radius: Float, depth: Float) -> ModelEntity {
var material = UnlitMaterial(color: wallColor)
if let texture = DirtTextureFactory.texture {
material.color = .init(tint: .white, texture: .init(texture))
}
material.faceCulling = .none
return ModelEntity(mesh: wallMesh(radius: radius, depth: depth), materials: [material])
}
/// Kreispunkte in der XZ-Ebene auf gegebener Höhe.
private static func circle(radius: Float, y: Float) -> [SIMD3<Float>] {
(0..<segments).map { index in
let angle = Float(index) / Float(segments) * 2 * .pi
return SIMD3(radius * cos(angle), y, radius * sin(angle))
}
}
/// Wandmantel (senkrecht, y = 0 … -depth) inkl. UV-Koordinaten für die gekachelte Textur.
private static func wallMesh(radius: Float, depth: Float) -> MeshResource {
let top = circle(radius: radius, y: 0)
let bottom = circle(radius: radius, y: -depth)
// Kachelgröße ~0,5 m: horizontal über den Umfang, vertikal über die Tiefe.
let tileSize: Float = 0.5
let uRepeat = (2 * .pi * radius) / tileSize
let vMax = depth / tileSize
var builder = TriangleMeshBuilder()
for i in 0..<segments {
let next = (i + 1) % segments
let u0 = Float(i) / Float(segments) * uRepeat
let u1 = Float(i + 1) / Float(segments) * uRepeat
builder.addQuad(
top[i], top[next], bottom[next], bottom[i],
uv: (SIMD2(u0, 0), SIMD2(u1, 0), SIMD2(u1, vMax), SIMD2(u0, vMax)),
doubleSided: true
)
}
return builder.makeMesh(name: "tankPitWall")
}
/// Runde Sohle bei y = -depth (Dreiecksfächer um den Mittelpunkt).
private static func floorMesh(radius: Float, depth: Float) -> MeshResource {
let rim = circle(radius: radius, y: -depth)
let center = SIMD3<Float>(0, -depth, 0)
var builder = TriangleMeshBuilder()
for i in 0..<segments {
let next = (i + 1) % segments
builder.addTriangle(center, rim[i], rim[next], doubleSided: true)
}
return builder.makeMesh(name: "tankPitFloor")
}
/// Flacher, hervorgehobener Ring (Rand) auf gegebener Höhe.
private static func ringMesh(radius: Float, y: Float) -> MeshResource {
let inner = circle(radius: max(0, radius - rimBand), y: y)
let outer = circle(radius: radius + rimBand, y: y)
var builder = TriangleMeshBuilder()
for i in 0..<segments {
let next = (i + 1) % segments
builder.addQuad(inner[i], inner[next], outer[next], outer[i], doubleSided: true)
}
return builder.makeMesh(name: "tankPitRim")
}
/// Waagerechte Fläche in der Bodenebene mit kreisrundem Loch (Ring zwischen Loch und Außenrand).
private static func maskMesh(holeRadius: Float, outerRadius: Float) -> MeshResource {
let inner = circle(radius: holeRadius, y: 0)
let outer = circle(radius: outerRadius, y: 0)
var builder = TriangleMeshBuilder()
for i in 0..<segments {
let next = (i + 1) % segments
builder.addQuad(inner[i], inner[next], outer[next], outer[i], doubleSided: true)
}
return builder.makeMesh(name: "tankPitMask")
}
}
/// Kleiner Helfer, der Dreiecke/Vierecke zu einem Mesh sammelt.
private struct TriangleMeshBuilder {
private var positions: [SIMD3<Float>] = []
private var indices: [UInt32] = []
private var uvs: [SIMD2<Float>] = []
/// Viereck mit UV-Koordinaten (für texturierte Flächen).
mutating func addQuad(
_ a: SIMD3<Float>, _ b: SIMD3<Float>, _ c: SIMD3<Float>, _ d: SIMD3<Float>,
uv: (SIMD2<Float>, SIMD2<Float>, SIMD2<Float>, SIMD2<Float>),
doubleSided: Bool = false
) {
let base = UInt32(positions.count)
positions.append(contentsOf: [a, b, c, d])
uvs.append(contentsOf: [uv.0, uv.1, uv.2, uv.3])
indices.append(contentsOf: [base, base + 1, base + 2, base, base + 2, base + 3])
if doubleSided {
indices.append(contentsOf: [base, base + 2, base + 1, base, base + 3, base + 2])
}
}
mutating func addTriangle(
_ a: SIMD3<Float>, _ b: SIMD3<Float>, _ c: SIMD3<Float>,
doubleSided: Bool = false
) {
let base = UInt32(positions.count)
positions.append(contentsOf: [a, b, c])
indices.append(contentsOf: [base, base + 1, base + 2])
if doubleSided {
indices.append(contentsOf: [base, base + 2, base + 1])
}
}
mutating func addQuad(
_ a: SIMD3<Float>, _ b: SIMD3<Float>, _ c: SIMD3<Float>, _ d: SIMD3<Float>,
doubleSided: Bool = false
) {
let base = UInt32(positions.count)
positions.append(contentsOf: [a, b, c, d])
indices.append(contentsOf: [base, base + 1, base + 2, base, base + 2, base + 3])
if doubleSided {
indices.append(contentsOf: [base, base + 2, base + 1, base, base + 3, base + 2])
}
}
func makeMesh(name: String) -> MeshResource {
var descriptor = MeshDescriptor(name: name)
descriptor.positions = MeshBuffers.Positions(positions)
descriptor.primitives = .triangles(indices)
if uvs.count == positions.count {
descriptor.textureCoordinates = MeshBuffers.TextureCoordinates(uvs)
}
return (try? MeshResource.generate(from: [descriptor])) ?? .generateBox(size: 0.01)
}
}