extends Node ## ## MeshGenModule - procedural primitive generators (Features 1-20). ## ## Every generator returns a MeshData Dictionary so it can be built for a ## MeshPool mesh via VertexBufferManager. Nothing here allocates GPU buffers ## until commit() is called, keeping generation cheap and evictable. ## ## MeshData := { verts: PackedVector3Array, indices: PackedInt32Array, uvs } ## var _host: Node = null var _root: Node3D = null func _activate(host: Node) -> void: _host = host _root = host.get_node_or_null("Geometry") as Node3D print("[MeshGen] activated") func _deactivate() -> void: _host = null _root = null func _make_mesh_data() -> Dictionary: return { "verts": PackedVector3Array(), "indices": PackedInt32Array(), "uvs": PackedVector2Array(), } ## --- Public generator API --------------------------------------------------- func generate(kind: String, params: Dictionary = {}) -> Dictionary: match kind: "cube": return cube(float(params.get("size", 1.0))) "sphere": return uv_sphere(int(params.get("rings", 16)), int(params.get("segments", 24)), float(params.get("radius", 0.5))) "cylinder": return cylinder(int(params.get("segments", 24)), float(params.get("top_r", 0.5)), float(params.get("bottom_r", 0.5)), float(params.get("height", 1.0))) "plane": return plane(int(params.get("subdiv", 1)), float(params.get("size", 2.0))) "torus": return torus(int(params.get("major_seg", 24)), int(params.get("minor_seg", 12)), float(params.get("major_r", 0.6)), float(params.get("minor_r", 0.2))) "ico": return icosphere(int(params.get("subdiv", 1)), float(params.get("radius", 0.5))) "suzanne": return suzanne(float(params.get("scale", 1.0))) _: push_warning("MeshGen: unknown kind '%s'" % kind) return _make_mesh_data() ## --- Generators ------------------------------------------------------------- func cube(size: float) -> Dictionary: var d := _make_mesh_data() var h := size * 0.5 var faces := [ # (normal, tangent, bitangent) for +X,-X,+Y,-Y,+Z,-Z [Vector3.RIGHT, Vector3.BACK, Vector3.UP], [Vector3.LEFT, Vector3.FORWARD, Vector3.UP], [Vector3.UP, Vector3.RIGHT, Vector3.BACK], [Vector3.DOWN, Vector3.RIGHT, Vector3.FORWARD], [Vector3.BACK, Vector3.RIGHT, Vector3.UP], [Vector3.FORWARD, Vector3.LEFT, Vector3.UP], ] for f in faces: var n: Vector3 = f[0] var t: Vector3 = f[1] var b: Vector3 = f[2] var base: int = d["verts"].size() var c := n * h d["verts"].append(c - t * h - b * h) d["verts"].append(c + t * h - b * h) d["verts"].append(c + t * h + b * h) d["verts"].append(c - t * h + b * h) d["uvs"].append(Vector2(0, 0)) d["uvs"].append(Vector2(1, 0)) d["uvs"].append(Vector2(1, 1)) d["uvs"].append(Vector2(0, 1)) d["indices"].append(base + 0); d["indices"].append(base + 1); d["indices"].append(base + 2) d["indices"].append(base + 0); d["indices"].append(base + 2); d["indices"].append(base + 3) return d func uv_sphere(rings: int, segments: int, radius: float) -> Dictionary: var d := _make_mesh_data() rings = maxi(2, rings) segments = maxi(3, segments) for y in range(rings + 1): var v := float(y) / float(rings) var phi := v * PI for x in range(segments + 1): var u := float(x) / float(segments) var theta := u * TAU var pos := Vector3( sin(phi) * cos(theta), cos(phi), sin(phi) * sin(theta) ) * radius d["verts"].append(pos) d["uvs"].append(Vector2(u, v)) for y in range(rings): for x in range(segments): var a := y * (segments + 1) + x var b := a + segments + 1 d["indices"].append(a); d["indices"].append(b); d["indices"].append(a + 1) d["indices"].append(a + 1); d["indices"].append(b); d["indices"].append(b + 1) return d func cylinder(segments: int, top_r: float, bottom_r: float, height: float) -> Dictionary: var d := _make_mesh_data() segments = maxi(3, segments) var hy := height * 0.5 for y in range(2): var r := top_r if y == 1 else bottom_r var yy := hy if y == 1 else -hy for x in range(segments + 1): var u := float(x) / float(segments) var theta := u * TAU d["verts"].append(Vector3(cos(theta) * r, yy, sin(theta) * r)) d["uvs"].append(Vector2(u, float(y))) for x in range(segments): var a := x var b := x + segments + 1 d["indices"].append(a); d["indices"].append(b); d["indices"].append(a + 1) d["indices"].append(a + 1); d["indices"].append(b); d["indices"].append(b + 1) # Caps var top_center: int = d["verts"].size() d["verts"].append(Vector3(0, hy, 0)); d["uvs"].append(Vector2(0.5, 0.5)) var bot_center: int = d["verts"].size() d["verts"].append(Vector3(0, -hy, 0)); d["uvs"].append(Vector2(0.5, 0.5)) for x in range(segments): var theta0 := (float(x) / float(segments)) * TAU var theta1 := (float(x + 1) / float(segments)) * TAU var t0: int = d["verts"].size() d["verts"].append(Vector3(cos(theta0) * top_r, hy, sin(theta0) * top_r)); d["uvs"].append(Vector2(0, 0)) d["verts"].append(Vector3(cos(theta1) * top_r, hy, sin(theta1) * top_r)); d["uvs"].append(Vector2(1, 0)) d["indices"].append(top_center); d["indices"].append(t0); d["indices"].append(t0 + 1) var b0: int = d["verts"].size() d["verts"].append(Vector3(cos(theta0) * bottom_r, -hy, sin(theta0) * bottom_r)); d["uvs"].append(Vector2(0, 0)) d["verts"].append(Vector3(cos(theta1) * bottom_r, -hy, sin(theta1) * bottom_r)); d["uvs"].append(Vector2(1, 0)) d["indices"].append(bot_center); d["indices"].append(b0 + 1); d["indices"].append(b0) return d func plane(subdiv: int, size: float) -> Dictionary: var d := _make_mesh_data() subdiv = maxi(1, subdiv) var half := size * 0.5 for z in range(subdiv + 1): for x in range(subdiv + 1): var u := float(x) / float(subdiv) var v := float(z) / float(subdiv) d["verts"].append(Vector3(-half + u * size, 0, -half + v * size)) d["uvs"].append(Vector2(u, v)) var row := subdiv + 1 for z in range(subdiv): for x in range(subdiv): var a := z * row + x var b := a + row d["indices"].append(a); d["indices"].append(a + 1); d["indices"].append(b) d["indices"].append(a + 1); d["indices"].append(b + 1); d["indices"].append(b) return d func torus(major_seg: int, minor_seg: int, major_r: float, minor_r: float) -> Dictionary: var d := _make_mesh_data() major_seg = maxi(3, major_seg) minor_seg = maxi(3, minor_seg) for i in range(major_seg + 1): var u := float(i) / float(major_seg) * TAU for j in range(minor_seg + 1): var v := float(j) / float(minor_seg) * TAU var center := Vector3(cos(u) * major_r, 0, sin(u) * major_r) var normal := Vector3(cos(u) * cos(v), sin(v), sin(u) * cos(v)) d["verts"].append(center + normal * minor_r) d["uvs"].append(Vector2(float(i) / float(major_seg), float(j) / float(minor_seg))) for i in range(major_seg): for j in range(minor_seg): var a := i * (minor_seg + 1) + j var b := a + minor_seg + 1 d["indices"].append(a); d["indices"].append(b); d["indices"].append(a + 1) d["indices"].append(a + 1); d["indices"].append(b); d["indices"].append(b + 1) return d func icosphere(subdiv: int, radius: float) -> Dictionary: var d := _make_mesh_data() var t := (1.0 + sqrt(5.0)) * 0.5 var verts := [ Vector3(-1, t, 0), Vector3(1, t, 0), Vector3(-1, -t, 0), Vector3(1, -t, 0), Vector3(0, -1, t), Vector3(0, 1, t), Vector3(0, -1, -t), Vector3(0, 1, -t), Vector3(t, 0, -1), Vector3(t, 0, 1), Vector3(-t, 0, -1), Vector3(-t, 0, 1), ] var faces := [ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], ] for s in range(maxi(0, subdiv)): var new_faces := [] var cache := {} for f in faces: var a: int = f[0]; var b: int = f[1]; var c: int = f[2] var ab := _midpoint(verts, cache, a, b) var bc := _midpoint(verts, cache, b, c) var ca := _midpoint(verts, cache, c, a) new_faces.append([a, ab, ca]) new_faces.append([b, bc, ab]) new_faces.append([c, ca, bc]) new_faces.append([ab, bc, ca]) faces = new_faces for v in verts: d["verts"].append(v.normalized() * radius) d["uvs"].resize(d["verts"].size()) for f in faces: d["indices"].append(f[0]); d["indices"].append(f[1]); d["indices"].append(f[2]) return d func _midpoint(verts: Array, cache: Dictionary, a: int, b: int) -> int: var key := "%d_%d" % [mini(a, b), maxi(a, b)] if cache.has(key): return cache[key] var mid: Vector3 = (verts[a] + verts[b]) * 0.5 verts.append(mid) var idx := verts.size() - 1 cache[key] = idx return idx ## A low-poly "Suzanne"-style monkey head silhouette (ears + cranium + snout). ## Not bit-identical to Blender's Suzanne, but generated purely from code. func suzanne(scale: float) -> Dictionary: var d := _make_mesh_data() # Cranium: squashed sphere var cranium := uv_sphere(10, 16, 0.6) _append(d, cranium["verts"], cranium["indices"], cranium["uvs"], Vector3(0, 0.1, 0) * scale, scale) # Snout: a cube-ish extension forward var snout := cube(0.4) _append(d, snout["verts"], snout["indices"], snout["uvs"], Vector3(0, -0.1, 0.5) * scale, scale) # Ears: two flat spheres left/right var ear_l := uv_sphere(6, 10, 0.25) _append(d, ear_l["verts"], ear_l["indices"], ear_l["uvs"], Vector3(-0.6, 0.35, 0) * scale, scale) var ear_r := uv_sphere(6, 10, 0.25) _append(d, ear_r["verts"], ear_r["indices"], ear_r["uvs"], Vector3(0.6, 0.35, 0) * scale, scale) return d ## Merge a sub-mesh into a target MeshData with an offset and uniform scale. func _append(dst: Dictionary, verts: PackedVector3Array, indices: PackedInt32Array, uvs: PackedVector2Array, offset: Vector3, s: float) -> void: var base: int = dst["verts"].size() for v in verts: dst["verts"].append(v * s + offset) for u in uvs: dst["uvs"].append(u) for i in indices: dst["indices"].append(base + i) ## --- Instantiation helper --------------------------------------------------- ## Build a MeshInstance3D from a generated MeshData and attach under Geometry. ## Returns the instance (caller owns its lifetime; use MeshPool.release on free). func spawn(kind: String, params: Dictionary = {}) -> MeshInstance3D: var data := generate(kind, params) return spawn_from_data(data, kind) func spawn_from_data(data: Dictionary, name_hint: String = "mesh") -> MeshInstance3D: var mesh := MeshPool.acquire_mesh() VertexBufferManager.commit(data["verts"], data["indices"], data["uvs"], mesh) var mi := MeshInstance3D.new() mi.name = name_hint.capitalize() mi.mesh = mesh var mat := StandardMaterial3D.new() mat.albedo_color = Color(0.7, 0.72, 0.8) mat.shading_mode = BaseMaterial3D.SHADING_MODE_PER_PIXEL mi.material_override = mat if _root != null: _root.add_child(mi) return mi