File size: 4,203 Bytes
84e8204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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