Files
Blockline/scripts/urban_mesh_batch.gd
T

109 lines
4.7 KiB
GDScript
Raw Normal View History

2026-09-12 11:01:37 +02:00
class_name UrbanMeshBatch
extends RefCounted
# Static city detail is merged into one mesh per material, rather than one node
# per bolt, brick or frame. All dimensions are metres. No physics is added here.
var _surfaces: Dictionary = {}
var _materials: Dictionary = {}
func _surface(kind: String, tint: Color) -> SurfaceTool:
var key := kind + tint.to_html()
if not _surfaces.has(key):
var tool := SurfaceTool.new()
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
_surfaces[key] = tool
_materials[key] = UrbanMaterials.get_surface(kind, tint)
return _surfaces[key]
func mesh(source: Mesh, position: Vector3, kind: String, tint: Color = Color.WHITE, angles: Vector3 = Vector3.ZERO, scale_value: Vector3 = Vector3.ONE) -> void:
var basis := Basis.from_euler(angles).scaled(scale_value)
var normalized := SurfaceTool.new()
normalized.create_from(source, 0)
normalized.deindex()
_surface(kind, tint).append_from(normalized.commit(), 0, Transform3D(basis, position))
func box(position: Vector3, size: Vector3, kind: String, tint: Color = Color.WHITE, angles: Vector3 = Vector3.ZERO) -> void:
var shape := BoxMesh.new()
shape.size = size
mesh(shape, position, kind, tint, angles)
func cylinder(position: Vector3, radius: float, height: float, kind: String, tint: Color = Color.WHITE, angles: Vector3 = Vector3.ZERO, top_radius: float = -1.0) -> void:
var shape := CylinderMesh.new()
shape.bottom_radius = radius
shape.top_radius = radius if top_radius < 0 else top_radius
shape.height = height
shape.radial_segments = 16
shape.rings = 1
mesh(shape, position, kind, tint, angles)
func sphere(position: Vector3, size: Vector3, kind: String, tint: Color = Color.WHITE) -> void:
var shape := SphereMesh.new()
shape.radius = 0.5
shape.height = 1.0
shape.radial_segments = 16
shape.rings = 8
mesh(shape, position, kind, tint, Vector3.ZERO, size)
func _triangle(tool: SurfaceTool, a: Vector3, b: Vector3, c: Vector3) -> void:
var normal := (c - a).cross(b - a).normalized()
for vertex in [a, b, c]:
tool.set_normal(normal)
tool.set_uv(Vector2(vertex.x + vertex.z, vertex.y))
tool.add_vertex(vertex)
func quad(a: Vector3, b: Vector3, c: Vector3, d: Vector3, kind: String, tint: Color = Color.WHITE, double_sided: bool = false) -> void:
var tool := _surface(kind, tint)
_triangle(tool, a, b, c)
_triangle(tool, a, c, d)
if double_sided:
_triangle(tool, c, b, a)
_triangle(tool, d, c, a)
# Profile points are (Z,Y). Bevel rings give sloping bodywork and softened
# concrete edges without a heavyweight external asset dependency.
func profile(points: PackedVector2Array, width: float, position: Vector3, kind: String, tint: Color = Color.WHITE, bevel: float = 0.06) -> void:
var center := Vector2.ZERO
for point in points:
center += point
center /= points.size()
var rings: Array[PackedVector3Array] = []
var safe_bevel := minf(bevel, width * 0.2)
for ring_index in 4:
var ring := PackedVector3Array()
var x: float = [-width * 0.5, -width * 0.5 + safe_bevel, width * 0.5 - safe_bevel, width * 0.5][ring_index]
for point in points:
var offset := (center - point).normalized() * safe_bevel if ring_index in [0, 3] else Vector2.ZERO
ring.append(position + Vector3(x, point.y + offset.y, point.x + offset.x))
rings.append(ring)
var surface := _surface(kind, tint)
for ring_index in 3:
for i in points.size():
var j := (i + 1) % points.size()
_triangle(surface, rings[ring_index][i], rings[ring_index + 1][j], rings[ring_index + 1][i])
_triangle(surface, rings[ring_index][i], rings[ring_index][j], rings[ring_index + 1][j])
var indices := Geometry2D.triangulate_polygon(points)
for i in range(0, indices.size(), 3):
_triangle(surface, rings[0][indices[i]], rings[0][indices[i + 2]], rings[0][indices[i + 1]])
_triangle(surface, rings[3][indices[i + 2]], rings[3][indices[i]], rings[3][indices[i + 1]])
func bevel_box(position: Vector3, size: Vector3, kind: String, tint: Color = Color.WHITE, bevel: float = 0.06) -> void:
var half_z := size.z * 0.5
var half_y := size.y * 0.5
var cut := minf(bevel, minf(half_y, half_z) * 0.35)
profile(PackedVector2Array([Vector2(-half_z + cut, -half_y), Vector2(half_z - cut, -half_y),
Vector2(half_z, -half_y + cut), Vector2(half_z, half_y - cut),
Vector2(half_z - cut, half_y), Vector2(-half_z + cut, half_y),
Vector2(-half_z, half_y - cut), Vector2(-half_z, -half_y + cut)]), size.x, position, kind, tint, cut)
func finish(parent: Node3D, name_prefix: String = "Static") -> void:
for key in _surfaces:
var tool: SurfaceTool = _surfaces[key]
tool.index()
var result := MeshInstance3D.new()
result.name = name_prefix + "_" + str(key)
result.mesh = tool.commit()
result.material_override = _materials[key]
parent.add_child(result)
_surfaces.clear()
_materials.clear()