OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / graph / graph_exporter.cpp

519 lines · cpp ref:
#include "graph_exporter.h"

#include "../nodes/attribute_nodes.h"
#include "../nodes/curve_nodes.h"
#include "../nodes/geometry_nodes.h"
#include "../nodes/input_nodes.h"
#include "../nodes/instance_nodes.h"
#include "../nodes/math_nodes.h"
#include "../nodes/mesh_ops_nodes.h"
#include "../nodes/mesh_primitive_nodes.h"
#include "../nodes/noise_nodes.h"
#include "../nodes/point_nodes.h"
#include "../nodes/selection_nodes.h"
#include "../nodes/utility_nodes.h"
#include "../nodes/volume_nodes.h"
#include "../nodes/zone_nodes.h"

#include <godot_cpp/classes/json.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

namespace godot {

void GraphExporter::_bind_methods() {
	ClassDB::bind_static_method("GraphExporter", D_METHOD("export_to_json", "graph"), &GraphExporter::export_to_json);
}

// Build the proc_type → Blender bl_type reverse map.
// Mirrors GEOMETRY_NODE_MAP from geometry_serializer.py.
static HashMap<StringName, String> _build_reverse_type_map() {
	HashMap<StringName, String> m;

	// Mesh Primitives
	m["MeshGrid"] = "GeometryNodeMeshGrid";
	// The SDF-grid family. Godot -> Blender must know these too, or the export
	// side and the import side disagree about what crosses -- which is what
	// test_node_maps asserts and what caught this.
	m["MeshToSDFGrid"] = "GeometryNodeMeshToSDFGrid";
	m["SDFGridFillet"] = "GeometryNodeSDFGridFillet";
	m["GridToMesh"] = "GeometryNodeGridToMesh";
	m["MeshCube"] = "GeometryNodeMeshCube";
	m["MeshCircle"] = "GeometryNodeMeshCircle";
	m["MeshCylinder"] = "GeometryNodeMeshCylinder";
	m["MeshUVSphere"] = "GeometryNodeMeshUVSphere";
	m["MeshIcoSphere"] = "GeometryNodeMeshIcoSphere";
	m["MeshLine"] = "GeometryNodeMeshLine";

	// Geometry Operations
	m["SetPosition"] = "GeometryNodeSetPosition";
	m["TransformGeometry"] = "GeometryNodeTransform";
	m["JoinGeometry"] = "GeometryNodeJoinGeometry";
	m["DeleteGeometry"] = "GeometryNodeDeleteGeometry";
	m["MergeByDistance"] = "GeometryNodeMergeByDistance";
	m["BoundingBox"] = "GeometryNodeBoundBox";
	m["SeparateComponents"] = "GeometryNodeSeparateComponents";
	m["SeparateGeometry"] = "GeometryNodeSeparateGeometry";
	m["DuplicateElements"] = "GeometryNodeDuplicateElements";

	// Mesh Operations
	m["FlipFaces"] = "GeometryNodeFlipFaces";
	m["SetShadeSmooth"] = "GeometryNodeSetShadeSmooth";
	// SIMPLE subdivision, not Catmull-Clark. SubdivideMeshNode splits each
	// triangle at its edge midpoints and moves nothing, which is exactly
	// GeometryNodeSubdivideMesh; GeometryNodeSubdivisionSurface is a different
	// operation and naming it here claimed a smoothing we do not do.
	m["SubdivideMesh"] = "GeometryNodeSubdivideMesh";
	// The graph's entry and exit. Both proc types have existed all along.
	m["GroupInput"] = "NodeGroupInput";
	m["GroupOutput"] = "NodeGroupOutput";
	m["ExtrudeMesh"] = "GeometryNodeExtrudeMesh";
	m["ScaleElements"] = "GeometryNodeScaleElements";
	m["DualMesh"] = "GeometryNodeDualMesh";

	// Curves
	m["CurveCircle"] = "GeometryNodeCurvePrimitiveCircle";
	m["CurveLine"] = "GeometryNodeCurvePrimitiveLine";
	m["BezierSegment"] = "GeometryNodeCurvePrimitiveBezierSegment";
	m["CurveToMesh"] = "GeometryNodeCurveToMesh";
	m["CurveToPoints"] = "GeometryNodeCurveToPoints";
	m["ResampleCurve"] = "GeometryNodeResampleCurve";
	m["TrimCurve"] = "GeometryNodeTrimCurve";

	// Points
	m["DistributePointsOnFaces"] = "GeometryNodeDistributePointsOnFaces";
	m["Points"] = "GeometryNodePoints";

	// Instances
	m["InstanceOnPoints"] = "GeometryNodeInstanceOnPoints";
	m["RotateInstances"] = "GeometryNodeRotateInstances";
	m["ScaleInstances"] = "GeometryNodeScaleInstances";
	m["TranslateInstances"] = "GeometryNodeTranslateInstances";
	m["RealizeInstances"] = "GeometryNodeRealizeInstances";

	// Math
	m["Math"] = "ShaderNodeMath";
	m["VectorMath"] = "ShaderNodeVectorMath";
	m["CombineXYZ"] = "ShaderNodeCombineXYZ";
	m["SeparateXYZ"] = "ShaderNodeSeparateXYZ";

	// Input
	m["Value"] = "ShaderNodeValue";
	m["Vector"] = "FunctionNodeInputVector";

	// Noise / Texture
	m["NoiseTexture"] = "ShaderNodeTexNoise";
	m["VoronoiTexture"] = "ShaderNodeTexVoronoi";
	m["WaveTexture"] = "ShaderNodeTexWave";
	m["WhiteNoise"] = "ShaderNodeTexWhiteNoise";
	m["GradientTexture"] = "ShaderNodeTexGradient";

	// Attribute / Field inputs
	m["Position"] = "GeometryNodeInputPosition";
	m["Normal"] = "GeometryNodeInputNormal";
	m["Index"] = "GeometryNodeInputIndex";

	// Zones
	m["RepeatZoneInput"] = "GeometryNodeRepeatInput";
	m["RepeatZoneOutput"] = "GeometryNodeRepeatOutput";
	m["SimulationZoneInput"] = "GeometryNodeSimulationInput";
	m["SimulationZoneOutput"] = "GeometryNodeSimulationOutput";
	m["ForeachZoneInput"] = "GeometryNodeForeachGeometryElementInput";
	m["ForeachZoneOutput"] = "GeometryNodeForeachGeometryElementOutput";


	// Field, utility and attribute layer -- mirrors the block added to
	// GEOMETRY_NODE_MAP. Both maps must agree; test/test_node_maps.py checks it.
	m["Compare"] = "FunctionNodeCompare";
	m["RandomValue"] = "FunctionNodeRandomValue";
	m["AccumulateField"] = "GeometryNodeAccumulateField";
	m["CaptureAttribute"] = "GeometryNodeCaptureAttribute";
	m["MeshBoolean"] = "GeometryNodeMeshBoolean";
	m["SampleIndex"] = "GeometryNodeSampleIndex";
	m["StoreNamedAttribute"] = "GeometryNodeStoreNamedAttribute";
	m["Switch"] = "GeometryNodeSwitch";
	m["Clamp"] = "ShaderNodeClamp";
	m["FloatCurve"] = "ShaderNodeFloatCurve";
	m["MapRange"] = "ShaderNodeMapRange";
	m["Mix"] = "ShaderNodeMix";
	m["DomainSize"] = "GeometryNodeAttributeDomainSize";
	m["NamedAttribute"] = "GeometryNodeInputNamedAttribute";
	m["RemoveNamedAttribute"] = "GeometryNodeRemoveAttribute";
	m["ColorRamp"] = "ShaderNodeValToRGB";
	m["RGBCurves"] = "ShaderNodeRGBCurve";


	// Rotation and matrix family.
	m["AxisAngleToRotation"] = "FunctionNodeAxisAngleToRotation";
	m["EulerToRotation"] = "FunctionNodeEulerToRotation";
	m["QuaternionToRotation"] = "FunctionNodeQuaternionToRotation";
	m["RotationToAxisAngle"] = "FunctionNodeRotationToAxisAngle";
	m["RotationToEuler"] = "FunctionNodeRotationToEuler";
	m["RotationToQuaternion"] = "FunctionNodeRotationToQuaternion";
	m["InvertRotation"] = "FunctionNodeInvertRotation";
	m["RotateRotation"] = "FunctionNodeRotateRotation";
	m["RotateVector"] = "FunctionNodeRotateVector";
	m["InputRotation"] = "FunctionNodeInputRotation";
	m["RotateEuler"] = "FunctionNodeRotateEuler";
	m["VectorRotate"] = "ShaderNodeVectorRotate";
	m["CombineMatrix"] = "FunctionNodeCombineMatrix";
	m["SeparateMatrix"] = "FunctionNodeSeparateMatrix";
	m["InvertMatrix"] = "FunctionNodeInvertMatrix";
	m["MatrixDeterminant"] = "FunctionNodeMatrixDeterminant";
	m["MatrixMultiply"] = "FunctionNodeMatrixMultiply";
	m["TransposeMatrix"] = "FunctionNodeTransposeMatrix";


	// Types that existed all along and were simply never mapped.
	m["BooleanMath"] = "FunctionNodeBooleanMath";
	m["ConvexHull"] = "GeometryNodeConvexHull";
	m["EdgePathsToCurves"] = "GeometryNodeEdgePathsToCurves";
	m["GeometryToInstance"] = "GeometryNodeGeometryToInstance";
	m["PointsToVertices"] = "GeometryNodePointsToVertices";
	m["SetCurveRadius"] = "GeometryNodeSetCurveRadius";
	m["SplitEdges"] = "GeometryNodeSplitEdges";
	m["Triangulate"] = "GeometryNodeTriangulate";

	return m;
}

// Convert a Variant default value to a JSON-friendly Variant (arrays for Vector3/Color).
static Variant _default_to_json(const Variant &p_val, SocketType p_type) {
	switch (p_type) {
		case SocketType::Vector: {
			if (p_val.get_type() == Variant::VECTOR3) {
				Vector3 v = p_val;
				Array a;
				a.push_back(v.x);
				a.push_back(v.y);
				a.push_back(v.z);
				return a;
			}
			return p_val;
		}
		case SocketType::Color: {
			if (p_val.get_type() == Variant::COLOR) {
				Color c = p_val;
				Array a;
				a.push_back(c.r);
				a.push_back(c.g);
				a.push_back(c.b);
				a.push_back(c.a);
				return a;
			}
			return p_val;
		}
		default:
			return p_val;
	}
}

// Extract node-specific properties into a Dictionary.
// This is the reverse of the property-application code in graph_importer.cpp.
static Dictionary _extract_properties(const Ref<ProcNode> &p_node) {
	Dictionary props;

	// --- operation property (MathNode, VectorMathNode, BooleanMathNode, CompareNode, MeshBooleanNode, VolumeBooleanNode) ---
	{
		MathNode *n = Object::cast_to<MathNode>(p_node.ptr());
		if (n) { props["operation"] = n->get_operation(); }
	}
	if (!props.has("operation")) {
		VectorMathNode *n = Object::cast_to<VectorMathNode>(p_node.ptr());
		if (n) { props["operation"] = n->get_operation(); }
	}
	if (!props.has("operation")) {
		BooleanMathNode *n = Object::cast_to<BooleanMathNode>(p_node.ptr());
		if (n) { props["operation"] = n->get_operation(); }
	}
	if (!props.has("operation")) {
		CompareNode *n = Object::cast_to<CompareNode>(p_node.ptr());
		if (n) { props["operation"] = n->get_operation(); }
	}
	if (!props.has("operation")) {
		MeshBooleanNode *n = Object::cast_to<MeshBooleanNode>(p_node.ptr());
		if (n) { props["operation"] = n->get_operation(); }
	}
	if (!props.has("operation")) {
		VolumeBooleanNode *n = Object::cast_to<VolumeBooleanNode>(p_node.ptr());
		if (n) { props["operation"] = n->get_operation(); }
	}

	// --- value property (ValueNode, IntegerNode, BooleanNode, StringNode, ColorNode, VectorNode) ---
	{
		ValueNode *n = Object::cast_to<ValueNode>(p_node.ptr());
		if (n) { props["value"] = n->get_value(); }
	}
	if (!props.has("value")) {
		IntegerNode *n = Object::cast_to<IntegerNode>(p_node.ptr());
		if (n) { props["value"] = n->get_value(); }
	}
	if (!props.has("value")) {
		BooleanNode *n = Object::cast_to<BooleanNode>(p_node.ptr());
		if (n) { props["value"] = n->get_value(); }
	}
	if (!props.has("value")) {
		StringNode *n = Object::cast_to<StringNode>(p_node.ptr());
		if (n) { props["value"] = Variant(n->get_value()); }
	}
	if (!props.has("value")) {
		ColorNode *n = Object::cast_to<ColorNode>(p_node.ptr());
		if (n) {
			Color c = n->get_value();
			Array a;
			a.push_back(c.r);
			a.push_back(c.g);
			a.push_back(c.b);
			a.push_back(c.a);
			props["value"] = a;
		}
	}
	if (!props.has("value")) {
		VectorNode *n = Object::cast_to<VectorNode>(p_node.ptr());
		if (n) {
			Vector3 v = n->get_value();
			Array a;
			a.push_back(v.x);
			a.push_back(v.y);
			a.push_back(v.z);
			props["value"] = a;
		}
	}

	// --- zone_id property (all zone node types) ---
	{
		RepeatZoneInputNode *n = Object::cast_to<RepeatZoneInputNode>(p_node.ptr());
		if (n) { props["zone_id"] = n->get_zone_id(); }
	}
	if (!props.has("zone_id")) {
		RepeatZoneOutputNode *n = Object::cast_to<RepeatZoneOutputNode>(p_node.ptr());
		if (n) { props["zone_id"] = n->get_zone_id(); }
	}
	if (!props.has("zone_id")) {
		ForeachZoneInputNode *n = Object::cast_to<ForeachZoneInputNode>(p_node.ptr());
		if (n) { props["zone_id"] = n->get_zone_id(); }
	}
	if (!props.has("zone_id")) {
		ForeachZoneOutputNode *n = Object::cast_to<ForeachZoneOutputNode>(p_node.ptr());
		if (n) { props["zone_id"] = n->get_zone_id(); }
	}
	if (!props.has("zone_id")) {
		SimulationZoneInputNode *n = Object::cast_to<SimulationZoneInputNode>(p_node.ptr());
		if (n) { props["zone_id"] = n->get_zone_id(); }
	}
	if (!props.has("zone_id")) {
		SimulationZoneOutputNode *n = Object::cast_to<SimulationZoneOutputNode>(p_node.ptr());
		if (n) { props["zone_id"] = n->get_zone_id(); }
	}

	// --- wave_type ---
	{
		WaveTextureNode *n = Object::cast_to<WaveTextureNode>(p_node.ptr());
		if (n) { props["wave_type"] = n->get_wave_type(); }
	}

	// --- gradient_type ---
	{
		GradientTextureNode *n = Object::cast_to<GradientTextureNode>(p_node.ptr());
		if (n) { props["gradient_type"] = n->get_gradient_type(); }
	}

	// --- Perlin noise properties (noise_type, noise_dimensions, normalize) ---
	{
		PerlinNoiseNode *n = Object::cast_to<PerlinNoiseNode>(p_node.ptr());
		if (n) {
			props["noise_type"] = n->get_noise_type();
			props["noise_dimensions"] = n->get_noise_dimensions();
			props["normalize"] = n->get_normalize() ? 1 : 0;
		}
	}

	// --- Color Ramp's stops, and the three enums that read them ---
	//
	// The Godot -> Blender direction. Without this a ramp authored here reached
	// Blender as whatever a fresh ShaderNodeValToRGB happens to be.
	{
		ColorRampNode *n = Object::cast_to<ColorRampNode>(p_node.ptr());
		if (n) {
			// Sent as an Array of numbers, which is what the JSON encoder and
			// the Python side both expect; a PackedFloat32Array would serialize
			// as a Godot-specific type.
			PackedFloat32Array flat = n->get_ramp_stops();
			Array stops;
			stops.resize(flat.size());
			for (int s = 0; s < flat.size(); s++) {
				stops[s] = flat[s];
			}
			props["ramp_stops"] = stops;
			props["interpolation"] = n->get_interpolation();
			props["color_mode"] = n->get_color_mode();
			props["hue_interpolation"] = n->get_hue_interpolation();
		}
	}

	// --- Float Curve / RGB Curves control points, the Godot -> Blender way ---
	{
		auto emit_curve = [&props](auto *n) {
			PackedFloat32Array pts = n->get_curve_points();
			Array a_pts;
			a_pts.resize(pts.size());
			for (int i = 0; i < pts.size(); i++) {
				a_pts[i] = pts[i];
			}
			props["curve_points"] = a_pts;
			PackedInt32Array h = n->get_curve_handles();
			Array a_h;
			a_h.resize(h.size());
			for (int i = 0; i < h.size(); i++) {
				a_h[i] = h[i];
			}
			props["curve_handles"] = a_h;
			PackedInt32Array c = n->get_curve_counts();
			Array a_c;
			a_c.resize(c.size());
			for (int i = 0; i < c.size(); i++) {
				a_c[i] = c[i];
			}
			props["curve_counts"] = a_c;
			props["extend"] = n->get_extend();
			props["use_clip"] = n->get_use_clip() ? 1 : 0;
			PackedFloat32Array r = n->get_clip_rect();
			Array a_r;
			a_r.resize(r.size());
			for (int i = 0; i < r.size(); i++) {
				a_r[i] = r[i];
			}
			props["clip_rect"] = a_r;
		};
		FloatCurveNode *fc = Object::cast_to<FloatCurveNode>(p_node.ptr());
		if (fc) {
			emit_curve(fc);
		}
		RGBCurvesNode *rc = Object::cast_to<RGBCurvesNode>(p_node.ptr());
		if (rc) {
			emit_curve(rc);
		}
	}

	// DisplaceNode: no properties to extract (always Perlin fBM)

	return props;
}

String GraphExporter::export_to_json(const Ref<ProcNodeGraph> &p_graph) {
	if (p_graph.is_null()) {
		UtilityFunctions::push_error("GraphExporter: graph is null");
		return "";
	}

	static HashMap<StringName, String> reverse_map = _build_reverse_type_map();

	const Vector<Ref<ProcNode>> &nodes = p_graph->get_nodes();
	const Vector<ProcConnection> &connections = p_graph->get_connections();

	// --- Nodes ---
	Array nodes_arr;
	for (int i = 0; i < nodes.size(); i++) {
		const Ref<ProcNode> &node = nodes[i];
		if (node.is_null()) {
			continue;
		}

		Dictionary node_dict;
		node_dict["id"] = i;

		StringName proc_type = node->get_node_type();
		node_dict["proc_type"] = String(proc_type);

		// Reverse-map to Blender type
		if (reverse_map.has(proc_type)) {
			node_dict["bl_type"] = reverse_map[proc_type];
		} else {
			node_dict["bl_type"] = ""; // Godot-only node, no Blender equivalent
		}

		node_dict["label"] = node->get_node_title();

		// Position: Godot Y-down → Blender Y-up (negate Y)
		Vector2 pos = node->get_graph_position();
		Array pos_arr;
		pos_arr.push_back(pos.x);
		pos_arr.push_back(-pos.y);
		node_dict["position"] = pos_arr;

		// Properties
		Dictionary props = _extract_properties(node);
		node_dict["properties"] = props;

		// Export ALL defaults (not just modified ones) because Blender's
		// constructor defaults may differ from Godot's
		Dictionary defaults;
		for (int j = 0; j < node->get_input_count(); j++) {
			const SocketDef &def = node->get_input_def(j);
			if (def.default_value.get_type() != Variant::NIL) {
				defaults[String::num_int64(j)] = _default_to_json(def.default_value, def.type);
			}
		}
		node_dict["defaults"] = defaults;

		// Socket NAMES, so the far side can resolve a socket by name instead of
		// trusting its index. An index does not identify a socket across the
		// bridge: an audit of the 53 node types mapped to Blender found 27 whose
		// layouts do not line up, most because Blender carries a Selection
		// socket at index 1 that proc-nodes has no equivalent for. SetPosition
		// is the plain case -- ours is (Geometry, Offset), Blender's is
		// (Geometry, Selection, Position, Offset), so our index 1 is its
		// Selection.
		Array inputs_arr;
		for (int j = 0; j < node->get_input_count(); j++) {
			const SocketDef &def = node->get_input_def(j);
			Dictionary sd;
			sd["name"] = String(def.name);
			sd["type"] = String(socket_type_name(def.type));
			inputs_arr.push_back(sd);
		}
		node_dict["inputs"] = inputs_arr;

		Array outputs_arr;
		for (int j = 0; j < node->get_output_count(); j++) {
			const SocketDef &def = node->get_output_def(j);
			Dictionary sd;
			sd["name"] = String(def.name);
			sd["type"] = String(socket_type_name(def.type));
			outputs_arr.push_back(sd);
		}
		node_dict["outputs"] = outputs_arr;

		nodes_arr.push_back(node_dict);
	}

	// --- Connections ---
	Array conns_arr;
	for (int i = 0; i < connections.size(); i++) {
		const ProcConnection &conn = connections[i];
		Dictionary conn_dict;
		conn_dict["from_node"] = conn.from_node;
		conn_dict["from_port"] = conn.from_socket;
		conn_dict["to_node"] = conn.to_node;
		conn_dict["to_port"] = conn.to_socket;
		conns_arr.push_back(conn_dict);
	}

	// --- Root dictionary ---
	Dictionary root;
	// 2: nodes carry "inputs"/"outputs" socket names. A reader that predates
	// this still works -- it just goes on resolving by index, which is what
	// version 1 meant.
	root["version"] = 2;
	root["nodes"] = nodes_arr;
	root["connections"] = conns_arr;

	String result = JSON::stringify(root);

	UtilityFunctions::print("GraphExporter: Exported ", nodes_arr.size(), " nodes, ",
			conns_arr.size(), " connections");

	return result;
}

} // namespace godot