// // 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) -> 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] { (0.. 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.. MeshResource { let rim = circle(radius: radius, y: -depth) let center = SIMD3(0, -depth, 0) var builder = TriangleMeshBuilder() for i in 0.. 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.. MeshResource { let inner = circle(radius: holeRadius, y: 0) let outer = circle(radius: outerRadius, y: 0) var builder = TriangleMeshBuilder() for i in 0..] = [] private var indices: [UInt32] = [] private var uvs: [SIMD2] = [] /// Viereck mit UV-Koordinaten (für texturierte Flächen). mutating func addQuad( _ a: SIMD3, _ b: SIMD3, _ c: SIMD3, _ d: SIMD3, uv: (SIMD2, SIMD2, SIMD2, SIMD2), 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, _ b: SIMD3, _ c: SIMD3, 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, _ b: SIMD3, _ c: SIMD3, _ d: SIMD3, 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) } }