mesh_resource_fbx.vala 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  1. /*
  2. * Copyright (c) 2012-2026 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public static int get_destination_file(out GLib.File destination_file
  8. , string destination_dir
  9. , GLib.File source_file
  10. )
  11. {
  12. string path = Path.build_filename(destination_dir, source_file.get_basename());
  13. destination_file = File.new_for_path(path);
  14. return 0;
  15. }
  16. public static int get_resource_path(out string resource_path
  17. , GLib.File destination_file
  18. , Project project
  19. )
  20. {
  21. string resource_filename = project.resource_filename(destination_file.get_path());
  22. resource_path = ResourceId.normalize(resource_filename);
  23. return 0;
  24. }
  25. public static Vector3 vector3(ufbx.Vec3 v)
  26. {
  27. return Vector3(v.x, v.y, v.z);
  28. }
  29. public static Quaternion quaternion(ufbx.Quat q)
  30. {
  31. return Quaternion(q.x, q.y, q.z, q.w);
  32. }
  33. public static string light_type(ufbx.LightType ufbx_type)
  34. {
  35. switch (ufbx_type) {
  36. case ufbx.LightType.DIRECTIONAL:
  37. return "directional";
  38. case ufbx.LightType.SPOT:
  39. return "spot";
  40. case ufbx.LightType.AREA:
  41. return "area";
  42. case ufbx.LightType.VOLUME:
  43. return "volume";
  44. case ufbx.LightType.POINT:
  45. default:
  46. return "omni";
  47. }
  48. }
  49. public static string projection_type(ufbx.ProjectionMode ufbx_mode)
  50. {
  51. switch (ufbx_mode) {
  52. case ufbx.ProjectionMode.ORTHOGRAPHIC:
  53. return "orthographic";
  54. case ufbx.ProjectionMode.PERSPECTIVE:
  55. default:
  56. return "perspective";
  57. }
  58. }
  59. [Compact]
  60. public class FBXImportOptions
  61. {
  62. public InputBool import_units;
  63. public InputBool import_lights;
  64. public InputBool import_cameras;
  65. public InputBool import_textures;
  66. public InputBool create_textures_folder;
  67. public InputBool import_materials;
  68. public InputBool create_materials_folder;
  69. public InputBool import_animation;
  70. public InputBool new_skeleton;
  71. public InputResource target_skeleton;
  72. public InputBool import_clips;
  73. public InputBool create_animations_folder;
  74. public FBXImportOptions(Database db)
  75. {
  76. import_units = new InputBool();
  77. import_units.value = true;
  78. import_units.value_changed.connect(on_import_units_changed);
  79. import_lights = new InputBool();
  80. import_lights.value = true;
  81. import_cameras = new InputBool();
  82. import_cameras.value = true;
  83. import_textures = new InputBool();
  84. import_textures.value = true;
  85. import_textures.value_changed.connect(on_import_textures_changed);
  86. create_textures_folder = new InputBool();
  87. create_textures_folder.value = true;
  88. import_materials = new InputBool();
  89. import_materials.value = true;
  90. import_materials.value_changed.connect(on_import_materials_changed);
  91. create_materials_folder = new InputBool();
  92. create_materials_folder.value = true;
  93. import_animation = new InputBool();
  94. import_animation.value = true;
  95. import_animation.value_changed.connect(on_import_animation_changed);
  96. new_skeleton = new InputBool();
  97. new_skeleton.value = true;
  98. new_skeleton.value_changed.connect(on_new_skeleton_changed);
  99. target_skeleton = new InputResource(OBJECT_TYPE_MESH_SKELETON, db);
  100. target_skeleton.sensitive = false;
  101. import_clips = new InputBool();
  102. import_clips.value = true;
  103. import_clips.value_changed.connect(on_import_animations_changed);
  104. create_animations_folder = new InputBool();
  105. create_animations_folder.value = true;
  106. }
  107. public void on_import_units_changed()
  108. {
  109. import_lights.sensitive = import_units.value;
  110. import_cameras.sensitive = import_units.value;
  111. import_textures.sensitive = import_units.value;
  112. create_textures_folder.sensitive = import_units.value;
  113. import_materials.sensitive = import_units.value;
  114. create_materials_folder.sensitive = import_units.value;
  115. }
  116. public void on_import_textures_changed()
  117. {
  118. create_textures_folder.set_sensitive(import_textures.value);
  119. }
  120. public void on_import_materials_changed()
  121. {
  122. create_materials_folder.set_sensitive(import_materials.value);
  123. }
  124. public void on_import_animations_changed()
  125. {
  126. create_animations_folder.set_sensitive(import_clips.value);
  127. }
  128. public void on_import_animation_changed()
  129. {
  130. new_skeleton.sensitive = import_animation.value;
  131. import_clips.sensitive = import_animation.value;
  132. create_animations_folder.sensitive = import_clips.value;
  133. }
  134. public void on_new_skeleton_changed()
  135. {
  136. target_skeleton.sensitive = !new_skeleton.value;
  137. }
  138. public void decode(Hashtable json)
  139. {
  140. json.foreach((g) => {
  141. if (g.key == "import_lights")
  142. import_lights.value = (bool)g.value;
  143. else if (g.key == "import_cameras")
  144. import_cameras.value = (bool)g.value;
  145. else if (g.key == "import_textures")
  146. import_textures.value = (bool)g.value;
  147. else if (g.key == "create_textures_folder")
  148. create_textures_folder.value = (bool)g.value;
  149. else if (g.key == "import_materials")
  150. import_materials.value = (bool)g.value;
  151. else if (g.key == "create_materials_folder")
  152. create_materials_folder.value = (bool)g.value;
  153. else if (g.key == "new_skeleton")
  154. new_skeleton.value = (bool)g.value;
  155. else if (g.key == "target_skeleton")
  156. target_skeleton.value = (string)g.value;
  157. else if (g.key == "import_clips")
  158. import_clips.value = (bool)g.value;
  159. else if (g.key == "create_animations_folder")
  160. create_animations_folder.value = (bool)g.value;
  161. else
  162. logw("Unknown option '%s'".printf(g.key));
  163. return true;
  164. });
  165. import_units.value = import_lights.value
  166. || import_cameras.value
  167. || import_textures.value
  168. || import_materials.value
  169. ;
  170. import_units.value_changed(import_animation);
  171. import_animation.value = new_skeleton.value
  172. || import_clips.value
  173. ;
  174. import_animation.value_changed(import_animation);
  175. }
  176. public Hashtable encode()
  177. {
  178. bool skip_units = !import_units.value;
  179. bool skip_anims = !import_animation.value;
  180. Hashtable obj = new Hashtable();
  181. obj.set("import_lights", skip_units ? false : import_lights.value);
  182. obj.set("import_cameras", skip_units ? false : import_cameras.value);
  183. obj.set("import_textures", skip_units ? false : import_textures.value);
  184. obj.set("create_textures_folder", skip_units ? false : create_textures_folder.value);
  185. obj.set("import_materials", skip_units ? false : import_materials.value);
  186. obj.set("create_materials_folder", skip_units ? false : create_materials_folder.value);
  187. obj.set("new_skeleton", skip_anims ? false : new_skeleton.value);
  188. obj.set("target_skeleton", skip_anims ? "" : target_skeleton.value);
  189. obj.set("import_clips", skip_anims ? false : import_clips.value);
  190. obj.set("create_animations_folder", skip_anims ? false : create_animations_folder.value);
  191. return obj;
  192. }
  193. }
  194. public class FBXImportDialog : Gtk.Window
  195. {
  196. public Project _project;
  197. public string _destination_dir;
  198. public Gee.ArrayList<string> _filenames;
  199. public unowned Import _import_result;
  200. public string _options_path;
  201. public FBXImportOptions _options;
  202. public PropertyGridSet _general_set;
  203. public Gtk.Box _box;
  204. public Gtk.Button _import;
  205. public Gtk.Button _cancel;
  206. public Gtk.HeaderBar _header_bar;
  207. public FBXImportDialog(Database database, string destination_dir, GLib.SList<string> filenames, Import import_result)
  208. {
  209. _project = database._project;
  210. _destination_dir = destination_dir;
  211. _filenames = new Gee.ArrayList<string>();
  212. foreach (var f in filenames)
  213. _filenames.add(f);
  214. _import_result = import_result;
  215. _general_set = new PropertyGridSet();
  216. _options = new FBXImportOptions(database);
  217. GLib.File file_dst;
  218. string resource_path;
  219. get_destination_file(out file_dst, destination_dir, File.new_for_path(_filenames[0]));
  220. get_resource_path(out resource_path, file_dst, _project);
  221. string resource_name = ResourceId.name(resource_path);
  222. _options_path = _project.absolute_path(resource_name) + ".importer_settings";
  223. try {
  224. _options.decode(SJSON.load_from_path(_options_path));
  225. } catch (JsonSyntaxError e) {
  226. // No-op.
  227. }
  228. PropertyGrid cv;
  229. cv = new PropertyGrid();
  230. cv.column_homogeneous = true;
  231. cv.add_row("Import Lights", _options.import_lights, "Import all light nodes.");
  232. cv.add_row("Import Cameras", _options.import_cameras, "Import all camera nodes.");
  233. cv.add_row("Import Textures", _options.import_textures, "Import all textures.");
  234. cv.add_row("Create Textures Folder", _options.create_textures_folder, "Put imported textures in a sub-folder.");
  235. cv.add_row("Import Materials", _options.import_materials, "Import all materials.");
  236. cv.add_row("Create Materials Folder", _options.create_materials_folder, "Put imported materials in a sub-folder.");
  237. _general_set.add_property_grid_optional(cv, "Units", _options.import_units, "Import nodes as units, materials and textures.");
  238. cv = new PropertyGrid();
  239. cv.column_homogeneous = true;
  240. cv.add_row("New Skeleton", _options.new_skeleton, "Create a new skeleton.");
  241. cv.add_row("Target Skeleton", _options.target_skeleton, "Skeleton to use.");
  242. cv.add_row("Import Animations", _options.import_clips, "Import all animation clips.");
  243. cv.add_row("Create Animations Folder", _options.create_animations_folder, "Put imported animations in a sub-folder.");
  244. _general_set.add_property_grid_optional(cv, "Animation", _options.import_animation, "Import animations and skeleton.");
  245. _box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  246. _box.pack_start(_general_set, false, false);
  247. _cancel = new Gtk.Button.with_label("Cancel");
  248. _cancel.clicked.connect(() => {
  249. close();
  250. });
  251. _import = new Gtk.Button.with_label("Import");
  252. _import.get_style_context().add_class("suggested-action");
  253. _import.clicked.connect(import);
  254. _header_bar = new Gtk.HeaderBar();
  255. _header_bar.title = "Import FBX...";
  256. _header_bar.show_close_button = true;
  257. _header_bar.pack_start(_cancel);
  258. _header_bar.pack_end(_import);
  259. _options.import_units.value_changed.connect(on_import_options_changed);
  260. _options.import_animation.value_changed.connect(on_import_options_changed);
  261. _options.new_skeleton.value_changed.connect(on_import_options_changed);
  262. _options.target_skeleton.value_changed.connect(on_import_options_changed);
  263. this.set_titlebar(_header_bar);
  264. this.add(_box);
  265. }
  266. void import()
  267. {
  268. ImportResult res = FBXImporter.do_import(_options, _project, _destination_dir, _filenames);
  269. if (res == ImportResult.SUCCESS) {
  270. try {
  271. SJSON.save(_options.encode(), _options_path);
  272. } catch (JsonWriteError e) {
  273. res = ImportResult.ERROR;
  274. }
  275. }
  276. _import_result(res);
  277. close();
  278. }
  279. void on_import_options_changed()
  280. {
  281. bool target_skeleton_is_valid = _options.new_skeleton.value
  282. || _options.target_skeleton.value != ""
  283. ;
  284. bool enable_import_button = (_options.import_units.value
  285. || _options.import_animation.value)
  286. && target_skeleton_is_valid
  287. ;
  288. _import.set_sensitive(enable_import_button);
  289. }
  290. }
  291. public class FBXImporter
  292. {
  293. public static void unit_create_components(FBXImportOptions options
  294. , Database db
  295. , Guid parent_unit_id
  296. , Guid unit_id
  297. , string resource_name
  298. , ufbx.Node node
  299. , Gee.HashMap<unowned ufbx.Material, string> imported_materials
  300. )
  301. {
  302. Vector3 pos = vector3(node.local_transform.translation);
  303. Quaternion rot = quaternion(node.local_transform.rotation);
  304. Vector3 scl = vector3(node.local_transform.scale);
  305. string editor_name = node.name.data.length == 0 ? OBJECT_NAME_UNNAMED : (string)node.name.data;
  306. // Create mesh_renderer.
  307. if (node.mesh != null) {
  308. Unit unit = Unit(db, unit_id);
  309. db.create(unit_id, OBJECT_TYPE_UNIT);
  310. db.set_name(unit_id, editor_name);
  311. // Create transform.
  312. {
  313. Guid component_id;
  314. if (!unit.has_component(out component_id, OBJECT_TYPE_TRANSFORM)) {
  315. component_id = Guid.new_guid();
  316. db.create(component_id, OBJECT_TYPE_TRANSFORM);
  317. db.add_to_set(unit_id, "components", component_id);
  318. }
  319. unit.set_component_vector3 (component_id, "data.position", pos);
  320. unit.set_component_quaternion(component_id, "data.rotation", rot);
  321. unit.set_component_vector3 (component_id, "data.scale", scl);
  322. unit.set_component_string (component_id, "data.name", editor_name);
  323. }
  324. if (node.mesh.num_faces > 0) {
  325. // Create mesh_renderer.
  326. {
  327. Guid component_id;
  328. if (!unit.has_component(out component_id, OBJECT_TYPE_MESH_RENDERER)) {
  329. component_id = Guid.new_guid();
  330. db.create(component_id, OBJECT_TYPE_MESH_RENDERER);
  331. db.add_to_set(unit_id, "components", component_id);
  332. }
  333. unowned ufbx.Material mesh_instance_material = node.materials.data[0];
  334. string material_name = "core/fallback/fallback";
  335. if (imported_materials.has_key(mesh_instance_material))
  336. material_name = imported_materials[mesh_instance_material];
  337. unit.set_component_string(component_id, "data.geometry_name", editor_name);
  338. unit.set_component_string(component_id, "data.material", material_name);
  339. unit.set_component_string(component_id, "data.mesh_resource", resource_name);
  340. unit.set_component_bool (component_id, "data.visible", true);
  341. }
  342. // Create collider.
  343. {
  344. Guid component_id;
  345. if (!unit.has_component(out component_id, OBJECT_TYPE_COLLIDER)) {
  346. component_id = Guid.new_guid();
  347. db.create(component_id, OBJECT_TYPE_COLLIDER);
  348. db.add_to_set(unit_id, "components", component_id);
  349. }
  350. unit.set_component_string(component_id, "data.shape", "mesh");
  351. unit.set_component_string(component_id, "data.scene", resource_name);
  352. unit.set_component_string(component_id, "data.name", editor_name);
  353. }
  354. // Create actor.
  355. {
  356. Guid component_id;
  357. if (!unit.has_component(out component_id, OBJECT_TYPE_ACTOR)) {
  358. component_id = Guid.new_guid();
  359. db.create(component_id, OBJECT_TYPE_ACTOR);
  360. db.add_to_set(unit_id, "components", component_id);
  361. }
  362. unit.set_component_string(component_id, "data.class", "static");
  363. unit.set_component_string(component_id, "data.collision_filter", "default");
  364. unit.set_component_double(component_id, "data.mass", 1.0);
  365. unit.set_component_string(component_id, "data.material", "default");
  366. }
  367. }
  368. } else if (node.light != null) {
  369. if (!options.import_lights.value)
  370. return;
  371. Unit unit = Unit(db, unit_id);
  372. unit.create("core/units/light");
  373. db.set_name(unit_id, editor_name);
  374. unit.set_local_position(pos);
  375. unit.set_local_rotation(rot);
  376. unit.set_local_scale(scl);
  377. Guid component_id;
  378. if (unit.has_component(out component_id, OBJECT_TYPE_LIGHT)) {
  379. unit.set_component_string (component_id, "data.type", light_type(node.light.type));
  380. unit.set_component_double (component_id, "data.range", 10.0);
  381. unit.set_component_double (component_id, "data.intensity", (double)node.light.intensity);
  382. unit.set_component_double (component_id, "data.spot_angle", 0.5 * MathUtils.rad((double)node.light.outer_angle));
  383. unit.set_component_vector3(component_id, "data.color", vector3(node.light.color));
  384. unit.set_component_double (component_id, "data.shadow_bias", 0.0001);
  385. unit.set_component_bool (component_id, "data.cast_shadows", node.light.cast_shadows);
  386. }
  387. } else if (node.camera != null) {
  388. if (!options.import_cameras.value)
  389. return;
  390. Unit unit = Unit(db, unit_id);
  391. unit.create("core/units/camera");
  392. db.set_name(unit_id, editor_name);
  393. unit.set_local_position(pos);
  394. unit.set_local_rotation(rot);
  395. unit.set_local_scale(scl);
  396. Guid component_id;
  397. if (unit.has_component(out component_id, OBJECT_TYPE_CAMERA)) {
  398. unit.set_component_string(component_id, "data.projection", projection_type(node.camera.projection_mode));
  399. unit.set_component_double(component_id, "data.fov", MathUtils.rad((double)node.camera.field_of_view_deg.y));
  400. unit.set_component_double(component_id, "data.far_range", (double)node.camera.far_plane);
  401. unit.set_component_double(component_id, "data.near_range", (double)node.camera.near_plane);
  402. }
  403. } else if (node.bone != null) {
  404. return;
  405. } else {
  406. Unit unit = Unit(db, unit_id);
  407. db.create(unit_id, OBJECT_TYPE_UNIT);
  408. db.set_name(unit_id, editor_name);
  409. // Create transform.
  410. Guid component_id;
  411. if (!unit.has_component(out component_id, OBJECT_TYPE_TRANSFORM)) {
  412. component_id = Guid.new_guid();
  413. db.create(component_id, OBJECT_TYPE_TRANSFORM);
  414. db.add_to_set(unit_id, "components", component_id);
  415. }
  416. unit.set_component_vector3 (component_id, "data.position", pos);
  417. unit.set_component_quaternion(component_id, "data.rotation", rot);
  418. unit.set_component_vector3 (component_id, "data.scale", scl);
  419. }
  420. if (parent_unit_id != GUID_ZERO)
  421. db.add_to_set(parent_unit_id, "children", unit_id);
  422. for (size_t i = 0; i < node.children.data.length; ++i) {
  423. unit_create_components(options
  424. , db
  425. , unit_id
  426. , Guid.new_guid()
  427. , resource_name
  428. , node.children.data[i]
  429. , imported_materials
  430. );
  431. }
  432. }
  433. public static unowned ufbx.Node? find_first_non_bone_parent(ufbx.Node? bone_node)
  434. {
  435. assert(bone_node != null);
  436. while (bone_node.bone != null)
  437. bone_node = bone_node.parent;
  438. return bone_node;
  439. }
  440. public static unowned ufbx.Node? find_skeleton_root(ufbx.Node? node)
  441. {
  442. if (node.bone != null)
  443. return node;
  444. for (size_t i = 0; i < node.children.data.length; ++i) {
  445. unowned ufbx.Node? n = find_skeleton_root(node.children.data[i]);
  446. if (n != null)
  447. return find_first_non_bone_parent(n);
  448. }
  449. return null;
  450. }
  451. public static void import_skeleton(FBXImportOptions options
  452. , Database db
  453. , Guid parent_bone_id
  454. , Guid bone_id
  455. , ufbx.Node node
  456. )
  457. {
  458. db.create(bone_id, OBJECT_TYPE_MESH_BONE);
  459. db.set_string(bone_id, "name", (string)node.name.data);
  460. if (parent_bone_id != GUID_ZERO)
  461. db.add_to_set(parent_bone_id, "children", bone_id);
  462. for (size_t i = 0; i < node.children.data.length; ++i) {
  463. unowned ufbx.Node child_bone = node.children.data[i];
  464. if (child_bone.bone == null)
  465. continue; // Skip non-bone children.
  466. import_skeleton(options
  467. , db
  468. , bone_id
  469. , Guid.new_guid()
  470. , child_bone
  471. );
  472. }
  473. }
  474. public static ImportResult do_import(FBXImportOptions options, Project project, string destination_dir, Gee.ArrayList<string> filenames)
  475. {
  476. foreach (string filename_i in filenames) {
  477. string resource_path;
  478. GLib.File file_dst;
  479. GLib.File file_src = File.new_for_path(filename_i);
  480. if (get_destination_file(out file_dst, destination_dir, file_src) != 0)
  481. return ImportResult.ERROR;
  482. if (get_resource_path(out resource_path, file_dst, project) != 0)
  483. return ImportResult.ERROR;
  484. string resource_name = ResourceId.name(resource_path);
  485. string resource_basename = GLib.File.new_for_path(resource_name).get_basename();
  486. // Copy FBX file.
  487. try {
  488. file_src.copy(file_dst, FileCopyFlags.OVERWRITE);
  489. } catch (Error e) {
  490. loge(e.message);
  491. return ImportResult.ERROR;
  492. }
  493. // Keep in sync with mesh_fbx.cpp!
  494. ufbx.LoadOpts load_opts = {};
  495. load_opts.target_camera_axes =
  496. {
  497. ufbx.CoordinateAxis.POSITIVE_X,
  498. ufbx.CoordinateAxis.POSITIVE_Z,
  499. ufbx.CoordinateAxis.NEGATIVE_Y
  500. };
  501. load_opts.target_light_axes =
  502. {
  503. ufbx.CoordinateAxis.POSITIVE_X,
  504. ufbx.CoordinateAxis.POSITIVE_Y,
  505. ufbx.CoordinateAxis.POSITIVE_Z
  506. };
  507. load_opts.target_axes = ufbx.CoordinateAxes.RIGHT_HANDED_Z_UP;
  508. load_opts.target_unit_meters = 1.0f;
  509. load_opts.space_conversion = ufbx.SpaceConversion.TRANSFORM_ROOT;
  510. // Load FBX file.
  511. ufbx.Error error = {};
  512. ufbx.Scene? scene = ufbx.Scene.load_file(filename_i, load_opts, ref error);
  513. Database db = new Database(project);
  514. Gee.HashMap<unowned ufbx.Texture, string> imported_textures = new Gee.HashMap<unowned ufbx.Texture, string>();
  515. Gee.HashMap<unowned ufbx.Material, string> imported_materials = new Gee.HashMap<unowned ufbx.Material, string>();
  516. // Import textures.
  517. if (options.import_units.value && options.import_textures.value) {
  518. // Create 'textures' folder.
  519. string directory_name = "textures";
  520. string textures_path = destination_dir;
  521. if (options.create_textures_folder.value && scene.textures.data.length != 0) {
  522. GLib.File textures_file = File.new_for_path(Path.build_filename(destination_dir, directory_name));
  523. try {
  524. textures_file.make_directory();
  525. } catch (GLib.IOError.EXISTS e) {
  526. // Ignore.
  527. } catch (GLib.Error e) {
  528. loge(e.message);
  529. return ImportResult.ERROR;
  530. }
  531. textures_path = textures_file.get_path();
  532. }
  533. // Import textures.
  534. for (size_t i = 0; i < scene.textures.data.length; ++i) {
  535. unowned ufbx.Texture texture = scene.textures.data[i];
  536. string source_image_filename = Path.build_filename(textures_path, (string)texture.name.data + ".png");
  537. GLib.File source_image_file = GLib.File.new_for_path(source_image_filename);
  538. string source_image_path = source_image_file.get_path();
  539. FileStream fs;
  540. // Extract embedded texture data or copy external
  541. // texture files into textures_path.
  542. if (texture.content.data.length > 0) {
  543. // Extract embedded PNG data.
  544. fs = FileStream.open(source_image_path, "wb");
  545. if (fs == null) {
  546. loge("Failed to open texture '%s'".printf(source_image_path));
  547. return ImportResult.ERROR;
  548. }
  549. size_t num_written = fs.write((uint8[])texture.content.data);
  550. if (num_written != texture.content.data.length) {
  551. loge("Failed to write texture '%s'".printf(source_image_path));
  552. return ImportResult.ERROR;
  553. }
  554. } else {
  555. // TODO: Copy external texture file.
  556. fs = null;
  557. }
  558. // Only create .texture resource if source image exists.
  559. if (fs == null) {
  560. logw("'%s' references non-existing texture '%s'".printf(filename_i, (string)texture.name.data));
  561. } else {
  562. string texture_resource_filename = project.resource_filename(source_image_path);
  563. string texture_resource_path = ResourceId.normalize(texture_resource_filename);
  564. string texture_resource_name = ResourceId.name(texture_resource_path);
  565. // Create .texture resource.
  566. Guid texture_id = Guid.new_guid();
  567. // FIXME: detect texture type.
  568. TextureResource texture_resource = TextureResource.color_map(db
  569. , texture_id
  570. , texture_resource_name + ".png"
  571. );
  572. if (texture_resource.save(project, texture_resource_name) != 0)
  573. return ImportResult.ERROR;
  574. imported_textures.set(texture, texture_resource_name);
  575. }
  576. }
  577. }
  578. // Import materials.
  579. if (options.import_units.value && options.import_materials.value) {
  580. // Create 'materials' folder.
  581. string directory_name = "materials";
  582. string materials_path = destination_dir;
  583. if (options.create_materials_folder.value && scene.materials.data.length != 0) {
  584. GLib.File materials_file = File.new_for_path(Path.build_filename(destination_dir, directory_name));
  585. try {
  586. materials_file.make_directory();
  587. } catch (GLib.IOError.EXISTS e) {
  588. // Ignore.
  589. } catch (GLib.Error e) {
  590. loge(e.message);
  591. return ImportResult.ERROR;
  592. }
  593. materials_path = materials_file.get_path();
  594. }
  595. // Extract materials.
  596. for (size_t i = 0; i < scene.materials.data.length; ++i) {
  597. unowned ufbx.Material material = scene.materials.data[i];
  598. string material_filename = Path.build_filename(materials_path, (string)material.name.data + ".png");
  599. GLib.File material_file = GLib.File.new_for_path(material_filename);
  600. string material_path = material_file.get_path();
  601. string material_resource_filename = project.resource_filename(material_path);
  602. string material_resource_path = ResourceId.normalize(material_resource_filename);
  603. string material_resource_name = ResourceId.name(material_resource_path);
  604. string shader = "mesh";
  605. Vector3 albedo = Vector3(1, 1, 1);
  606. double metallic = 0.0;
  607. double roughness = 1.0;
  608. Vector3 emission_color = Vector3(0, 0, 0);
  609. double emission_intensity = 1.0;
  610. string? albedo_map = null;
  611. string? normal_map = null;
  612. string? metallic_map = null;
  613. string? roughness_map = null;
  614. string? ao_map = null;
  615. string? emission_map = null;
  616. for (int mm = 0; mm < ufbx.MaterialPbrMap.MAP_COUNT; ++mm) {
  617. unowned ufbx.MaterialMap map = material.pbr.maps[mm];
  618. switch (mm) {
  619. case ufbx.MaterialPbrMap.BASE_COLOR: {
  620. albedo = vector3(map.value_vec3);
  621. // Lookup matching imported texture, if any.
  622. if (map.texture_enabled
  623. && map.texture != null
  624. && imported_textures.has_key(map.texture)
  625. ) {
  626. albedo_map = imported_textures[map.texture];
  627. }
  628. break;
  629. }
  630. case ufbx.MaterialPbrMap.NORMAL_MAP: {
  631. // Lookup matching imported texture, if any.
  632. if (map.texture_enabled
  633. && map.texture != null
  634. && imported_textures.has_key(map.texture)
  635. ) {
  636. normal_map = imported_textures[map.texture];
  637. }
  638. break;
  639. }
  640. case ufbx.MaterialPbrMap.METALNESS: {
  641. metallic = map.value_real;
  642. // Lookup matching imported texture, if any.
  643. if (map.texture_enabled
  644. && map.texture != null
  645. && imported_textures.has_key(map.texture)
  646. ) {
  647. metallic_map = imported_textures[map.texture];
  648. }
  649. break;
  650. }
  651. case ufbx.MaterialPbrMap.ROUGHNESS: {
  652. roughness = map.value_real;
  653. // Lookup matching imported texture, if any.
  654. if (map.texture_enabled
  655. && map.texture != null
  656. && imported_textures.has_key(map.texture)
  657. ) {
  658. roughness_map = imported_textures[map.texture];
  659. }
  660. break;
  661. }
  662. case ufbx.MaterialPbrMap.AMBIENT_OCCLUSION: {
  663. // Lookup matching imported texture, if any.
  664. if (map.texture_enabled
  665. && map.texture != null
  666. && imported_textures.has_key(map.texture)
  667. ) {
  668. ao_map = imported_textures[map.texture];
  669. }
  670. break;
  671. }
  672. case ufbx.MaterialPbrMap.EMISSION_COLOR: {
  673. emission_color = vector3(map.value_vec3);
  674. // Lookup matching imported texture, if any.
  675. if (map.texture_enabled
  676. && map.texture != null
  677. && imported_textures.has_key(map.texture)
  678. ) {
  679. emission_map = imported_textures[map.texture];
  680. }
  681. break;
  682. }
  683. case ufbx.MaterialPbrMap.EMISSION_FACTOR:
  684. emission_intensity = (double)map.value_real;
  685. break;
  686. default:
  687. break;
  688. }
  689. }
  690. if (options.import_animation.value)
  691. shader += "+SKINNING";
  692. // Create .material resource.
  693. MaterialResource material_resource = MaterialResource.mesh(db
  694. , Guid.new_guid()
  695. , albedo_map
  696. , normal_map
  697. , metallic_map
  698. , roughness_map
  699. , ao_map
  700. , emission_map
  701. , albedo
  702. , metallic
  703. , roughness
  704. , emission_color
  705. , emission_intensity
  706. , shader
  707. );
  708. if (material_resource.save(project, material_resource_name) != 0)
  709. return ImportResult.ERROR;
  710. imported_materials.set(material, material_resource_name);
  711. }
  712. }
  713. // Import animations.
  714. StateMachineResource? smr = null;
  715. if (options.import_animation.value) {
  716. string target_skeleton = options.target_skeleton.value;
  717. // Import animation skeleton.
  718. if (options.new_skeleton.value) {
  719. // Create .animation_skeleton resource.
  720. unowned ufbx.Node? skeleton_root_node = find_skeleton_root(scene.root_node);
  721. if (skeleton_root_node != null) {
  722. Guid skeleton_hierarchy_id = Guid.new_guid();
  723. import_skeleton(options
  724. , db
  725. , GUID_ZERO
  726. , skeleton_hierarchy_id
  727. , skeleton_root_node
  728. );
  729. Guid animation_skeleton_id = Guid.new_guid();
  730. db.create(animation_skeleton_id, OBJECT_TYPE_MESH_SKELETON);
  731. db.set_string(animation_skeleton_id, "source", resource_path);
  732. db.add_to_set(animation_skeleton_id, "skeleton", skeleton_hierarchy_id);
  733. if (db.save(project.absolute_path(resource_name) + "." + OBJECT_TYPE_MESH_SKELETON, animation_skeleton_id) != 0)
  734. return ImportResult.ERROR;
  735. target_skeleton = resource_name;
  736. // Create .state_machine resource to drive the skeleton.
  737. Guid state_machine_id = Guid.new_guid();
  738. smr = StateMachineResource.mesh(db
  739. , state_machine_id
  740. , target_skeleton
  741. );
  742. if (smr.save(project, resource_name) != 0)
  743. return ImportResult.ERROR;
  744. }
  745. }
  746. // Import animation clip.
  747. if (options.import_clips.value) {
  748. if (target_skeleton == "") {
  749. logw("Animation must have a target skeleton. Animation clips won't be imported.");
  750. } else {
  751. // Create 'animations' folder.
  752. string directory_name = "animations";
  753. string animations_path = destination_dir;
  754. if (options.create_animations_folder.value && scene.anim_stacks.data.length != 0) {
  755. GLib.File animations_file = File.new_for_path(Path.build_filename(destination_dir, directory_name));
  756. try {
  757. animations_file.make_directory();
  758. } catch (GLib.IOError.EXISTS e) {
  759. // Ignore.
  760. } catch (GLib.Error e) {
  761. loge(e.message);
  762. return ImportResult.ERROR;
  763. }
  764. animations_path = animations_file.get_path();
  765. }
  766. // Extract clips.
  767. if (scene.anim_stacks.data.length > 0) {
  768. unowned ufbx.AnimStack anim_stack = scene.anim_stacks.data[0];
  769. string anim_filename = Path.build_filename(animations_path, resource_basename + "." + OBJECT_TYPE_MESH_ANIMATION);
  770. GLib.File anim_file = GLib.File.new_for_path(anim_filename);
  771. string anim_path = anim_file.get_path();
  772. string anim_resource_filename = project.resource_filename(anim_path);
  773. string anim_resource_path = ResourceId.normalize(anim_resource_filename);
  774. string anim_resource_name = ResourceId.name(anim_resource_path);
  775. // Create .mesh_animation resource.
  776. Guid anim_id = Guid.new_guid();
  777. db.create(anim_id, OBJECT_TYPE_MESH_ANIMATION);
  778. db.set_string(anim_id, "source", resource_path);
  779. db.set_string(anim_id, "target_skeleton", target_skeleton);
  780. db.set_string(anim_id, "stack_name", (string)anim_stack.name.data);
  781. if (db.save(project.absolute_path(anim_resource_name) + "." + OBJECT_TYPE_MESH_ANIMATION, anim_id) != 0)
  782. return ImportResult.ERROR;
  783. }
  784. }
  785. }
  786. }
  787. if (options.import_units.value) {
  788. // Generate or modify existing .unit.
  789. Guid unit_id = Guid.new_guid();
  790. unit_create_components(options
  791. , db
  792. , GUID_ZERO
  793. , unit_id
  794. , resource_name
  795. , scene.root_node
  796. , imported_materials
  797. );
  798. if (options.import_animation.value && options.new_skeleton.value && smr != null) {
  799. // Create animation_state_machine component.
  800. Unit unit = Unit(db, unit_id);
  801. Guid component_id;
  802. if (!unit.has_component(out component_id, OBJECT_TYPE_ANIMATION_STATE_MACHINE)) {
  803. component_id = Guid.new_guid();
  804. db.create(component_id, OBJECT_TYPE_ANIMATION_STATE_MACHINE);
  805. db.add_to_set(unit_id, "components", component_id);
  806. }
  807. unit.set_component_string(component_id, "data.state_machine_resource", resource_name);
  808. }
  809. if (db.save(project.absolute_path(resource_name) + ".unit", unit_id) != 0)
  810. return ImportResult.ERROR;
  811. Guid mesh_id = Guid.new_guid();
  812. db.create(mesh_id, OBJECT_TYPE_MESH);
  813. db.set_string(mesh_id, "source", resource_path);
  814. if (db.save(project.absolute_path(resource_name) + ".mesh", mesh_id) != 0)
  815. return ImportResult.ERROR;
  816. }
  817. }
  818. return ImportResult.SUCCESS;
  819. }
  820. public static void import(Import import_result, Database database, string destination_dir, GLib.SList<string> filenames, Gtk.Window? parent_window)
  821. {
  822. FBXImportDialog dialog = new FBXImportDialog(database, destination_dir, filenames, import_result);
  823. dialog.set_transient_for(parent_window);
  824. dialog.set_modal(true);
  825. dialog.show_all();
  826. dialog.present();
  827. }
  828. }
  829. } /* namespace Crown */