extends Node ## ## VertexBufferManager - VRAM-aware helpers for building and editing meshes. ## ## Two jobs: ## 1. estimate_cost_mb(): predict a mesh's RAM/VRAM footprint *before* build so ## MemoryManager can decide to evict something else first. ## 2. surface builders that operate on PackedArrays (CPU side) and upload once, ## avoiding per-vertex set_vertex() calls which are catastrophic on mobile. ## ## Mobile rule of thumb used here: ONE glTF/ArrayMesh upload per edit, never a ## stream of tiny updates. Sculpt strokes batch into a dirty region and flush. ## ## Bytes per vertex for the attributes we upload: position(12) + normal(12) + uv(8) const BYTES_PER_VERTEX := 32 ## Extra overhead for indices (~4 bytes/vert on a typical closed mesh). const INDEX_OVERHEAD := 4 ## Conservative VRAM residency multiplier (driver copies, alignment, etc.). const VRAM_FACTOR := 1.35 ## Estimate the RAM+VRAM cost of a mesh with `vertex_count` vertices in MB. func estimate_cost_mb(vertex_count: int) -> float: var bytes := float(vertex_count) * float(BYTES_PER_VERTEX + INDEX_OVERHEAD) return (bytes / 1048576.0) * VRAM_FACTOR ## Build an ArrayMesh from parallel PackedArrays in a single surface upload. ## `arrays` must follow Godot's Mesh.ARRAY_* layout. func build_surface(vertices: PackedVector3Array, normals: PackedVector3Array, uvs: PackedVector2Array, indices: PackedInt32Array, mesh: ArrayMesh = null) -> ArrayMesh: var out := mesh if out == null: out = MeshPool.acquire_mesh() var arrays: Array = [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = vertices arrays[Mesh.ARRAY_NORMAL] = normals arrays[Mesh.ARRAY_TEX_UV] = uvs arrays[Mesh.ARRAY_INDEX] = indices out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) return out ## Replace the geometry of an existing mesh in place (single edit surface). ## Keeps the same ArrayMesh object so MeshPool accounting stays correct. func update_surface(mesh: ArrayMesh, surface_idx: int, vertices: PackedVector3Array, normals: PackedVector3Array, uvs: PackedVector2Array, indices: PackedInt32Array) -> void: mesh.clear_surfaces() build_surface(vertices, normals, uvs, indices, mesh) # Recompute AABB for correct culling; without this, culling breaks after # large edits and the object may vanish from the frustum. mesh.custom_aabb = _compute_aabb(vertices) func _compute_aabb(vertices: PackedVector3Array) -> AABB: if vertices.is_empty(): return AABB() var mn := vertices[0] var mx := vertices[0] for v in vertices: mn = mn.min(v) mx = mx.max(v) return AABB(mn, mx - mn) ## Recompute smooth normals for a triangle soup. Used after sculpting/vertex ## edits where normals must reflect the new shape (otherwise lighting freezes). func recompute_normals(vertices: PackedVector3Array, indices: PackedInt32Array) -> PackedVector3Array: var normals := PackedVector3Array() normals.resize(vertices.size()) for i in range(normals.size()): normals[i] = Vector3.ZERO var tri := indices.size() / 3 for t in range(tri): var i0 := indices[t * 3] var i1 := indices[t * 3 + 1] var i2 := indices[t * 3 + 2] var n := (vertices[i1] - vertices[i0]).cross(vertices[i2] - vertices[i0]) normals[i0] = normals[i0] + n normals[i1] = normals[i1] + n normals[i2] = normals[i2] + n for i in range(normals.size()): var n := normals[i] if n.length_squared() > 0.000001: normals[i] = n.normalized() else: normals[i] = Vector3.UP return normals ## Convenience: full rebuild + normal recompute + upload in one call. func commit(vertices: PackedVector3Array, indices: PackedInt32Array, uvs: PackedVector2Array, mesh: ArrayMesh) -> ArrayMesh: var normals := recompute_normals(vertices, indices) update_surface(mesh, 0, vertices, normals, uvs, indices) return mesh func debug_vram_mb(meshes: Array) -> float: var total := 0.0 for m in meshes: if m is ArrayMesh: var vcount := 0 for s in range(m.get_surface_count()): var arr: Array = m.surface_get_arrays(s) if arr.size() > Mesh.ARRAY_VERTEX and arr[Mesh.ARRAY_VERTEX] != null: vcount += (arr[Mesh.ARRAY_VERTEX] as PackedVector3Array).size() total += estimate_cost_mb(vcount) return total