OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / core / proc_mesh.h

166 lines · c ref:
#ifndef PROC_NODES_PROC_MESH_H
#define PROC_NODES_PROC_MESH_H

#include "attribute_storage.h"
#include <godot_cpp/classes/array_mesh.hpp>
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
#include <godot_cpp/variant/packed_color_array.hpp>
#include <godot_cpp/variant/packed_float32_array.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/packed_vector2_array.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/packed_vector3_array.hpp>

namespace godot {

class ProcMesh : public RefCounted {
	GDCLASS(ProcMesh, RefCounted);

protected:
	static void _bind_methods();

public:
	// Built-in arrays
	PackedVector3Array positions;
	PackedVector3Array normals;
	PackedFloat32Array tangents; // x,y,z,sign per corner
	PackedVector2Array uvs;
	PackedColorArray colors;
	PackedInt32Array indices; // Triangle indices

	// Build from an engine mesh, so geometry authored outside a graph -- an
	// object's own mesh, a bridged cage -- can be fed INTO one. The reverse,
	// to_array_mesh, has always existed; this direction had not, which is why
	// nothing could supply a graph's Group Input.
	static Ref<ProcMesh> from_array_mesh(const Ref<Mesh> &p_mesh);

	// THE MESH CHANNEL. A mesh as both authoring systems define one -- points,
	// a corner array, faces as corner ranges, edges, and named attributes on
	// their domains -- carried in a Dictionary so blender-bridge can hand one
	// over through ClassDB reflection without a compile-time dependency.
	//
	// ⛔ NOT an ArrayMesh. An ArrayMesh has ONE domain, the vertex, plus an
	// index buffer: a face, corner or edge value has nowhere to live in it, and
	// a corner normal can only be expressed by duplicating the vertex. Every
	// mesh that crossed the bridge as an ArrayMesh arrived triangulated and
	// de-indexed -- the mountain's 10088-vertex quad cage as 60516 loose
	// corners -- and no graph could ask it a topological question.
	//
	//     points      PackedVector3Array   positions, sender's frame
	//     corners     PackedInt32Array     vertex index per corner
	//     faces       PackedInt32Array     face_count + 1 offsets into corners
	//     edges       PackedInt32Array     2 per edge (the SENDER's order)
	//     attributes  Array[Dictionary]    {name, domain, type, data}
	//
	// See blender-bridge/docs/mesh-channel-design.md.
	static Ref<ProcMesh> from_mesh_data(const Dictionary &p_data);
	Dictionary to_mesh_data() const;
	PackedInt32Array face_offsets; // Face → corner range (OffsetIndices)

	// Named attributes per domain
	AttributeStorage point_attrs;
	AttributeStorage corner_attrs;
	AttributeStorage face_attrs;
	AttributeStorage edge_attrs;

	// Domain sizes
	int vertex_count() const { return positions.size(); }
	int triangle_count() const { return indices.size() / 3; }
	int face_count() const;
	int corner_count() const;

	// THE EDGE DOMAIN. ProcMesh stores positions, triangles and face offsets;
	// it has never stored edges, so `domain_size(Edge)` returned 0 with a TODO
	// and the whole edge domain was unreachable -- Split Edges was a
	// pass-through, Merge By Distance had no Connected mode, and a per-edge
	// attribute had nowhere to live.
	//
	// Edges are DERIVED from the triangles rather than stored, and derived
	// DETERMINISTICALLY -- each undirected pair normalised low-to-high and the
	// set sorted -- so the same mesh always yields the same edge ORDER and a
	// per-edge attribute means the same thing twice. Not cached: a cache would
	// need invalidating on every topology change, and there is no single place
	// those go through. Correctness first; it can be cached when it is hot.
	//
	// The order is OURS, not Blender's: Blender builds an icosphere's edges in
	// its own sequence, so the COUNT agrees and index i need not be the same
	// edge. Said here so nobody asserts the stronger thing.
	PackedInt32Array compute_edges() const; // 2 ints per edge, v0 < v1
	int edge_count() const;

	// NGONS. `indices` is the CORNER array and `face_offsets` groups it into
	// faces -- offsets[f] .. offsets[f+1] are face f's corners, so it has
	// face_count + 1 entries. EMPTY face_offsets still means "every face is a
	// triangle", which is what every existing mesh here is, so nothing changes
	// meaning by adding this.
	//
	// It matters because Blender keeps QUADS and ngons: a 2x2 grid is one face
	// there and two triangles here, so face, corner and edge counts diverge,
	// a per-face value has nowhere to agree, and the subdivided mountain's
	// vertices land in slightly different places.
	bool has_ngons() const { return face_offsets.size() > 1; }
	int face_corner_begin(int p_face) const {
		return has_ngons() ? face_offsets[p_face] : p_face * 3;
	}
	int face_size(int p_face) const {
		return has_ngons() ? (face_offsets[p_face + 1] - face_offsets[p_face]) : 3;
	}
	// Faces fanned into triangles, for anything that must have them -- the
	// renderer, and the GPU path. A quad becomes two triangles about corner 0,
	// which is what Blender's own triangulation does for a convex quad.
	PackedInt32Array triangulated_indices() const;

	int domain_size(AttrDomain p_domain) const;

	// Convert to Godot ArrayMesh
	Ref<ArrayMesh> to_array_mesh() const;

	// Update an existing ArrayMesh in place (avoids full resource recreation).
	// Returns false if topology changed (caller should use to_array_mesh instead).
	// The existing mesh must have exactly one surface with matching vertex/index counts.
	bool update_array_mesh(Ref<ArrayMesh> p_mesh) const;

	// Reverse every face's corner order, permuting corner-domain values with
	// it. A face is a list of corners and that order is the only thing that
	// says which side is the front -- Blender and Godot read it oppositely,
	// so a mesh authored in one has to be turned round to enter the other.
	void reverse_faces();

	// Recalculate normals from the FACES, in Godot's winding.
	void recalculate_normals();
	// The one implementation both that and to_array_mesh() use.
	void compute_normals(PackedVector3Array &r_out) const;

	// Pack positions + normals (+ UVs if present) into Godot's internal vertex
	// buffer format for use with surface_update_vertex_region().
	// p_stride / p_pos_offset / p_normal_offset / p_uv_offset come from
	// RenderingServer::mesh_surface_get_format_vertex_stride/offset queries.
	// Offsets set to -1 mean "attribute not present".
	PackedByteArray pack_vertex_buffer(uint32_t p_stride,
			uint32_t p_pos_offset, uint32_t p_normal_offset,
			int32_t p_uv_offset) const;

	// Deep copy
	Ref<ProcMesh> duplicate_mesh() const;

	// INSTRUMENT ONLY. to_array_mesh() is per-VERTEX, so a corner-, face- or
	// edge-domain value cannot leave this class through it -- which is why the
	// UV Map outputs could not be compared against Blender at all. These hand
	// the raw domain to GDScript so a probe can dump it.
	PackedInt32Array get_indices() const { return indices; }
	PackedInt32Array get_face_offsets() const { return face_offsets; }
	int get_corner_count() const { return corner_count(); }
	int get_face_count() const { return face_count(); }
	int get_domain_size(int p_domain) const { return domain_size(static_cast<AttrDomain>(p_domain)); }
	// p_domain is AttrDomain: 0 Point, 1 Edge, 2 Face, 3 Corner.
	Array get_attribute_values(int p_domain, const StringName &p_name) const;
	PackedStringArray get_attribute_names(int p_domain) const;

	ProcMesh() = default;
};

} // namespace godot

#endif // PROC_NODES_PROC_MESH_H