Initial commit for first version of project

This commit is contained in:
Dennis Nemec
2026-09-02 20:54:36 +02:00
parent 3d9f1ab6bc
commit 992bb32025
56 changed files with 5139 additions and 31 deletions

View File

@ -0,0 +1,211 @@
//
// 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)
}
}