Add Chromium-only Blender WebEngine parity work
This commit is contained in:
50
blender-5.2.0/source/blender/io/fbx/CMakeLists.txt
Normal file
50
blender-5.2.0/source/blender/io/fbx/CMakeLists.txt
Normal file
@@ -0,0 +1,50 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
.
|
||||
importer
|
||||
../common
|
||||
../../editors/include
|
||||
../../makesrna
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
IO_fbx.cc
|
||||
importer/fbx_import.cc
|
||||
importer/fbx_import_anim.cc
|
||||
importer/fbx_import_armature.cc
|
||||
importer/fbx_import_material.cc
|
||||
importer/fbx_import_mesh.cc
|
||||
importer/fbx_import_util.cc
|
||||
|
||||
IO_fbx.hh
|
||||
importer/fbx_import.hh
|
||||
importer/fbx_import_anim.hh
|
||||
importer/fbx_import_armature.hh
|
||||
importer/fbx_import_material.hh
|
||||
importer/fbx_import_mesh.hh
|
||||
importer/fbx_import_util.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
PRIVATE bf::animrig
|
||||
PRIVATE bf::blenkernel
|
||||
PRIVATE bf::blenlib
|
||||
PRIVATE bf::blentranslation
|
||||
PRIVATE bf::bmesh
|
||||
PRIVATE bf::depsgraph
|
||||
PRIVATE bf::dna
|
||||
PRIVATE bf::nodes
|
||||
PRIVATE bf::imbuf
|
||||
PRIVATE bf::intern::clog
|
||||
PRIVATE bf::intern::guardedalloc
|
||||
bf_io_common
|
||||
PRIVATE bf::extern::ufbx
|
||||
)
|
||||
|
||||
blender_add_lib(bf_io_fbx "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
41
blender-5.2.0/source/blender/io/fbx/IO_fbx.cc
Normal file
41
blender-5.2.0/source/blender/io/fbx/IO_fbx.cc
Normal file
@@ -0,0 +1,41 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BLI_timeit.hh"
|
||||
|
||||
#include "BKE_context.hh"
|
||||
#include "BKE_layer.hh"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
#include "fbx_import.hh"
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace blender {
|
||||
|
||||
using namespace blender::timeit;
|
||||
|
||||
static void report_duration(const char *job, const TimePoint &start_time, const char *path)
|
||||
{
|
||||
Nanoseconds duration = Clock::now() - start_time;
|
||||
fmt::print("FBX {} of '{}' took ", job, BLI_path_basename(path));
|
||||
print_duration(duration);
|
||||
fmt::print("\n");
|
||||
}
|
||||
|
||||
void FBX_import(bContext *C, const FBXImportParams ¶ms)
|
||||
{
|
||||
TimePoint start_time = Clock::now();
|
||||
Main *bmain = CTX_data_main(C);
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
ViewLayer *view_layer = CTX_data_view_layer(C);
|
||||
io::fbx::importer_main(bmain, scene, view_layer, params);
|
||||
report_duration("import", start_time, params.filepath);
|
||||
}
|
||||
|
||||
} // namespace blender
|
||||
58
blender-5.2.0/source/blender/io/fbx/IO_fbx.hh
Normal file
58
blender-5.2.0/source/blender/io/fbx/IO_fbx.hh
Normal file
@@ -0,0 +1,58 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
#include "DNA_ID.h"
|
||||
|
||||
#include "IO_orientation.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Mesh;
|
||||
struct bContext;
|
||||
struct ReportList;
|
||||
|
||||
/**
|
||||
* Behavior when the name of an imported material
|
||||
* conflicts with an existing material.
|
||||
*/
|
||||
enum class eFBXMtlNameCollisionMode {
|
||||
MakeUnique = 0,
|
||||
ReferenceExisting = 1,
|
||||
};
|
||||
|
||||
enum class eFBXVertexColorMode {
|
||||
None = 0,
|
||||
sRGB = 1,
|
||||
Linear = 2,
|
||||
};
|
||||
|
||||
struct FBXImportParams {
|
||||
char filepath[FILE_MAX] = "";
|
||||
float global_scale = 1.0f;
|
||||
eFBXMtlNameCollisionMode mtl_name_collision_mode = eFBXMtlNameCollisionMode::MakeUnique;
|
||||
eFBXVertexColorMode vertex_colors = eFBXVertexColorMode::sRGB;
|
||||
bool validate_meshes = true;
|
||||
bool use_custom_normals = true;
|
||||
bool import_subdivision = false;
|
||||
bool use_custom_props = true;
|
||||
bool props_enum_as_string = true;
|
||||
bool ignore_leaf_bones = false;
|
||||
|
||||
bool use_anim = true;
|
||||
float anim_offset = 1.0f;
|
||||
|
||||
ReportList *reports = nullptr;
|
||||
};
|
||||
|
||||
void FBX_import(bContext *C, const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace blender
|
||||
478
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.cc
Normal file
478
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.cc
Normal file
@@ -0,0 +1,478 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_camera.h"
|
||||
#include "BKE_layer.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_library.hh"
|
||||
#include "BKE_light.h"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "BLI_fileops.h"
|
||||
#include "BLI_math_rotation.h"
|
||||
#include "BLI_task.hh"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
#include "DEG_depsgraph_build.hh"
|
||||
|
||||
#include "DNA_camera_types.h"
|
||||
#include "DNA_collection_types.h"
|
||||
#include "DNA_light_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_scene_types.h"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
|
||||
#include "fbx_import.hh"
|
||||
#include "fbx_import_anim.hh"
|
||||
#include "fbx_import_armature.hh"
|
||||
#include "fbx_import_material.hh"
|
||||
#include "fbx_import_mesh.hh"
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static CLG_LogRef LOG = {"io.fbx"};
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
struct FbxImportContext {
|
||||
Main *bmain;
|
||||
const ufbx_scene &fbx;
|
||||
const FBXImportParams ¶ms;
|
||||
std::string base_dir;
|
||||
FbxElementMapping mapping;
|
||||
|
||||
FbxImportContext(Main *main, const ufbx_scene *fbx, const FBXImportParams ¶ms)
|
||||
: bmain(main), fbx(*fbx), params(params)
|
||||
{
|
||||
char basedir[FILE_MAX];
|
||||
BLI_path_split_dir_part(params.filepath, basedir, sizeof(basedir));
|
||||
base_dir = basedir;
|
||||
|
||||
ufbx_transform root_tr;
|
||||
root_tr.translation = ufbx_zero_vec3;
|
||||
root_tr.rotation = this->fbx.metadata.root_rotation;
|
||||
root_tr.scale.x = root_tr.scale.y = root_tr.scale.z = this->fbx.metadata.root_scale;
|
||||
this->mapping.global_conv_matrix = ufbx_transform_to_matrix(&root_tr);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
std::string debug_file_path = params.filepath;
|
||||
debug_file_path = debug_file_path.substr(0, debug_file_path.size() - 4) + "-dbg-b.txt";
|
||||
g_debug_file = BLI_fopen(debug_file_path.c_str(), "wb");
|
||||
#endif
|
||||
}
|
||||
|
||||
~FbxImportContext()
|
||||
{
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
if (g_debug_file) {
|
||||
fclose(g_debug_file);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void import_globals(Scene *scene) const;
|
||||
void import_materials();
|
||||
void import_meshes();
|
||||
void import_cameras();
|
||||
void import_lights();
|
||||
void import_empties();
|
||||
void import_armatures();
|
||||
void import_animation(double fps);
|
||||
|
||||
void setup_hierarchy();
|
||||
};
|
||||
|
||||
void FbxImportContext::import_globals(Scene *scene) const
|
||||
{
|
||||
/* Set scene frame-rate to that of FBX file. */
|
||||
double fps = this->fbx.settings.frames_per_second;
|
||||
scene->r.frs_sec = roundf(fps);
|
||||
scene->r.frs_sec_base = scene->r.frs_sec / fps;
|
||||
}
|
||||
|
||||
void FbxImportContext::import_materials()
|
||||
{
|
||||
for (const ufbx_material *fmat : this->fbx.materials) {
|
||||
Material *mat = nullptr;
|
||||
/* Check if a material with this name already exists in the main database */
|
||||
if (this->params.mtl_name_collision_mode == eFBXMtlNameCollisionMode::ReferenceExisting) {
|
||||
mat = (Material *)BKE_libblock_find_name(this->bmain, ID_MA, fmat->name.data);
|
||||
}
|
||||
|
||||
if (mat == nullptr) {
|
||||
mat = io::fbx::import_material(this->bmain, this->base_dir, *fmat);
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(fmat->props, mat->id, this->params.props_enum_as_string);
|
||||
}
|
||||
}
|
||||
this->mapping.mat_to_material.add(fmat, mat);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_meshes()
|
||||
{
|
||||
io::fbx::import_meshes(*this->bmain, this->fbx, this->mapping, this->params);
|
||||
}
|
||||
|
||||
static bool should_import_camera(const ufbx_scene &fbx, const ufbx_camera *camera)
|
||||
{
|
||||
BLI_assert(camera->instances.count > 0);
|
||||
const ufbx_node *node = camera->instances[0];
|
||||
/* Files produced by MotionBuilder have several cameras at the root,
|
||||
* which just map to "viewports" and should not get imported. */
|
||||
if (node->node_depth == 1 && node->children.count == 0 &&
|
||||
STREQ("MotionBuilder", fbx.metadata.original_application.name.data))
|
||||
{
|
||||
if (STREQ(node->name.data, camera->name.data)) {
|
||||
if (STREQ("Producer Perspective", node->name.data) ||
|
||||
STREQ("Producer Front", node->name.data) || STREQ("Producer Back", node->name.data) ||
|
||||
STREQ("Producer Right", node->name.data) || STREQ("Producer Left", node->name.data) ||
|
||||
STREQ("Producer Top", node->name.data) || STREQ("Producer Bottom", node->name.data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FbxImportContext::import_cameras()
|
||||
{
|
||||
for (const ufbx_camera *fcam : this->fbx.cameras) {
|
||||
if (fcam->instances.count == 0) {
|
||||
continue; /* Ignore if not used by any objects. */
|
||||
}
|
||||
if (!should_import_camera(this->fbx, fcam)) {
|
||||
continue;
|
||||
}
|
||||
const ufbx_node *node = fcam->instances[0];
|
||||
|
||||
Camera *bcam = BKE_camera_add(this->bmain, get_fbx_name(fcam->name, "Camera"));
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(fcam->props, bcam->id, this->params.props_enum_as_string);
|
||||
}
|
||||
|
||||
bcam->type = fcam->projection_mode == UFBX_PROJECTION_MODE_ORTHOGRAPHIC ? CAM_ORTHO :
|
||||
CAM_PERSP;
|
||||
bcam->dof.focus_distance = ufbx_find_real(&fcam->props, "FocusDistance", 10.0f) *
|
||||
this->fbx.metadata.geometry_scale * this->fbx.metadata.root_scale;
|
||||
if (ufbx_find_bool(&fcam->props, "UseDepthOfField", false)) {
|
||||
bcam->dof.flag |= CAM_DOF_ENABLED;
|
||||
}
|
||||
bcam->lens = fcam->focal_length_mm;
|
||||
constexpr double m_to_in = 0.0393700787;
|
||||
bcam->sensor_x = fcam->film_size_inch.x / m_to_in;
|
||||
bcam->sensor_y = fcam->film_size_inch.y / m_to_in;
|
||||
|
||||
/* Note: do not use `fcam->orthographic_extent` to match Python importer behavior, which was
|
||||
* not taking ortho units into account. */
|
||||
bcam->ortho_scale = ufbx_find_real(&fcam->props, "OrthoZoom", 1.0);
|
||||
|
||||
bcam->shiftx = ufbx_find_real(&fcam->props, "FilmOffsetX", 0.0) / (m_to_in * bcam->sensor_x);
|
||||
bcam->shifty = ufbx_find_real(&fcam->props, "FilmOffsetY", 0.0) / (m_to_in * bcam->sensor_x);
|
||||
bcam->clip_start = fcam->near_plane * this->fbx.metadata.root_scale;
|
||||
bcam->clip_end = fcam->far_plane * this->fbx.metadata.root_scale;
|
||||
|
||||
Object *obj = BKE_object_add_only_object(this->bmain, OB_CAMERA, get_fbx_name(node->name));
|
||||
obj->data = id_cast<ID *>(bcam);
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_lights()
|
||||
{
|
||||
for (const ufbx_light *flight : this->fbx.lights) {
|
||||
if (flight->instances.count == 0) {
|
||||
continue; /* Ignore if not used by any objects. */
|
||||
}
|
||||
const ufbx_node *node = flight->instances[0];
|
||||
|
||||
Light *lamp = BKE_light_add(this->bmain, get_fbx_name(flight->name, "Light"));
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(flight->props, lamp->id, this->params.props_enum_as_string);
|
||||
}
|
||||
switch (flight->type) {
|
||||
case UFBX_LIGHT_POINT:
|
||||
lamp->type = LA_LOCAL;
|
||||
break;
|
||||
case UFBX_LIGHT_DIRECTIONAL:
|
||||
lamp->type = LA_SUN;
|
||||
break;
|
||||
case UFBX_LIGHT_SPOT:
|
||||
lamp->type = LA_SPOT;
|
||||
lamp->spotsize = DEG2RAD(flight->outer_angle);
|
||||
lamp->spotblend = 1.0f - flight->inner_angle / flight->outer_angle;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
lamp->r = flight->color.x;
|
||||
lamp->g = flight->color.y;
|
||||
lamp->b = flight->color.z;
|
||||
lamp->energy = flight->intensity;
|
||||
lamp->exposure = ufbx_find_real(&flight->props, "Exposure", 0.0);
|
||||
if (flight->cast_shadows) {
|
||||
lamp->mode |= LA_SHADOW;
|
||||
}
|
||||
//@TODO: if hasattr(lamp, "cycles"): lamp.cycles.cast_shadow = lamp.use_shadow
|
||||
|
||||
Object *obj = BKE_object_add_only_object(this->bmain, OB_LAMP, get_fbx_name(node->name));
|
||||
obj->data = id_cast<ID *>(lamp);
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_armatures()
|
||||
{
|
||||
io::fbx::import_armatures(*this->bmain, this->fbx, this->mapping, this->params);
|
||||
}
|
||||
|
||||
void FbxImportContext::import_empties()
|
||||
{
|
||||
/* Create empties for fbx nodes. */
|
||||
for (const ufbx_node *node : this->fbx.nodes) {
|
||||
/* Ignore root, bones and nodes for which we have created objects already. */
|
||||
if (node->is_root || this->mapping.node_is_blender_bone.contains(node) ||
|
||||
this->mapping.el_to_object.contains(&node->element))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
/* Ignore nodes at root for cameras (normally already imported, except for ignored cameras)
|
||||
* and camera switchers. */
|
||||
if (ELEM(node->attrib_type, UFBX_ELEMENT_CAMERA, UFBX_ELEMENT_CAMERA_SWITCHER) &&
|
||||
node->node_depth == 1 && node->children.count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Object *obj = BKE_object_add_only_object(this->bmain, OB_EMPTY, get_fbx_name(node->name));
|
||||
obj->data = nullptr;
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_animation(double fps)
|
||||
{
|
||||
if (this->params.use_anim) {
|
||||
io::fbx::import_animations(
|
||||
*this->bmain, this->fbx, this->mapping, fps, this->params.anim_offset);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::setup_hierarchy()
|
||||
{
|
||||
for (const auto &item : this->mapping.el_to_object.items()) {
|
||||
if (item.value->parent != nullptr) {
|
||||
continue; /* Parent is already set up (e.g. armature). */
|
||||
}
|
||||
const ufbx_node *node = ufbx_as_node(item.key);
|
||||
if (node == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (node->parent) {
|
||||
Object *obj_par = this->mapping.el_to_object.lookup_default(&node->parent->element, nullptr);
|
||||
if (!ELEM(obj_par, nullptr, item.value)) {
|
||||
item.value->parent = obj_par;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fbx_task_run_fn(void * /* user */,
|
||||
ufbx_thread_pool_context ctx,
|
||||
uint32_t /* group */,
|
||||
uint32_t start_index,
|
||||
uint32_t count)
|
||||
{
|
||||
threading::parallel_for_each(IndexRange(start_index, count), [&](const int64_t index) {
|
||||
ufbx_thread_pool_run_task(ctx, index);
|
||||
});
|
||||
}
|
||||
|
||||
static void fbx_task_wait_fn(void * /* user */,
|
||||
ufbx_thread_pool_context /* ctx */,
|
||||
uint32_t /* group */,
|
||||
uint32_t /* max_index */)
|
||||
{
|
||||
/* Empty implementation; #fbx_task_run_fn already waits for the tasks.
|
||||
* This means that only one fbx "task group" is effectively scheduled at once. */
|
||||
}
|
||||
|
||||
void importer_main(Main *bmain, Scene *scene, ViewLayer *view_layer, const FBXImportParams ¶ms)
|
||||
{
|
||||
FILE *file = BLI_fopen(params.filepath, "rb");
|
||||
if (!file) {
|
||||
CLOG_ERROR(&LOG, "Failed to open FBX file '%s'", params.filepath);
|
||||
BKE_reportf(params.reports, RPT_ERROR, "FBX Import: Cannot open file '%s'", params.filepath);
|
||||
return;
|
||||
}
|
||||
|
||||
ufbx_load_opts opts = {};
|
||||
opts.filename.data = params.filepath;
|
||||
opts.filename.length = strlen(params.filepath);
|
||||
opts.evaluate_skinning = false;
|
||||
opts.evaluate_caches = false;
|
||||
opts.load_external_files = false;
|
||||
opts.clean_skin_weights = true;
|
||||
opts.use_blender_pbr_material = true;
|
||||
|
||||
opts.geometry_transform_handling = UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY;
|
||||
opts.pivot_handling = UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT;
|
||||
|
||||
opts.space_conversion = UFBX_SPACE_CONVERSION_ADJUST_TRANSFORMS;
|
||||
opts.target_axes.right = UFBX_COORDINATE_AXIS_POSITIVE_X;
|
||||
opts.target_axes.up = UFBX_COORDINATE_AXIS_POSITIVE_Z;
|
||||
opts.target_axes.front = UFBX_COORDINATE_AXIS_NEGATIVE_Y;
|
||||
opts.target_unit_meters = 1.0f / params.global_scale;
|
||||
|
||||
opts.target_camera_axes.right = UFBX_COORDINATE_AXIS_POSITIVE_X;
|
||||
opts.target_camera_axes.up = UFBX_COORDINATE_AXIS_POSITIVE_Y;
|
||||
opts.target_camera_axes.front = UFBX_COORDINATE_AXIS_POSITIVE_Z;
|
||||
opts.target_light_axes.right = UFBX_COORDINATE_AXIS_POSITIVE_X;
|
||||
opts.target_light_axes.up = UFBX_COORDINATE_AXIS_POSITIVE_Y;
|
||||
opts.target_light_axes.front = UFBX_COORDINATE_AXIS_POSITIVE_Z;
|
||||
|
||||
/* Setup ufbx threading to go through our own task system. */
|
||||
opts.thread_opts.pool.run_fn = fbx_task_run_fn;
|
||||
opts.thread_opts.pool.wait_fn = fbx_task_wait_fn;
|
||||
|
||||
ufbx_error fbx_error;
|
||||
ufbx_scene *fbx = ufbx_load_stdio(file, &opts, &fbx_error);
|
||||
fclose(file);
|
||||
|
||||
if (!fbx) {
|
||||
CLOG_ERROR(&LOG,
|
||||
"Failed to import FBX file '%s': '%s'\n",
|
||||
params.filepath,
|
||||
fbx_error.description.data);
|
||||
BKE_reportf(params.reports,
|
||||
RPT_ERROR,
|
||||
"FBX Import: Cannot import file '%s': '%s'",
|
||||
params.filepath,
|
||||
fbx_error.description.data);
|
||||
return;
|
||||
}
|
||||
|
||||
LayerCollection *lc = BKE_layer_collection_get_active_editable(view_layer);
|
||||
if (!ID_IS_EDITABLE(lc->collection)) {
|
||||
BKE_report(params.reports,
|
||||
RPT_WARNING,
|
||||
"Could not find an editable collection in current scene, imported data will not be "
|
||||
"instantiated");
|
||||
}
|
||||
//@TODO: do we need to sort objects by name? (faster to create within blender)
|
||||
|
||||
FbxImportContext ctx(bmain, fbx, params);
|
||||
ctx.import_globals(scene);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
{
|
||||
fprintf(g_debug_file, "Initial NODE local matrices:\n");
|
||||
Vector<const ufbx_node *> nodes;
|
||||
for (const ufbx_node *node : ctx.fbx.nodes) {
|
||||
if (node->is_root) {
|
||||
continue;
|
||||
}
|
||||
nodes.append(node);
|
||||
}
|
||||
std::ranges::sort(nodes, [](const ufbx_node *a, const ufbx_node *b) {
|
||||
int ncmp = strcmp(a->name.data, b->name.data);
|
||||
if (ncmp != 0) {
|
||||
return ncmp < 0;
|
||||
}
|
||||
return a->attrib_type > b->attrib_type;
|
||||
});
|
||||
for (const ufbx_node *node : nodes) {
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(node->node_depth < 2 ? &node->node_to_world :
|
||||
&node->node_to_parent,
|
||||
&node->geometry_to_node);
|
||||
fprintf(g_debug_file, "init NODE %s self.matrix:\n", node->name.data);
|
||||
print_matrix(mtx);
|
||||
}
|
||||
fprintf(g_debug_file, "\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
ctx.import_materials();
|
||||
ctx.import_armatures();
|
||||
ctx.import_meshes();
|
||||
ctx.import_cameras();
|
||||
ctx.import_lights();
|
||||
ctx.import_empties();
|
||||
ctx.import_animation(scene->frames_per_second());
|
||||
ctx.setup_hierarchy();
|
||||
|
||||
ufbx_free_scene(fbx);
|
||||
|
||||
/* Add objects to collection. */
|
||||
for (Object *obj : ctx.mapping.imported_objects) {
|
||||
BKE_collection_object_add(bmain, lc->collection, obj);
|
||||
}
|
||||
|
||||
/* Select objects, sync layers etc. */
|
||||
BKE_view_layer_base_deselect_all(*bmain, scene, view_layer);
|
||||
BKE_view_layer_synced_ensure(*bmain, scene, view_layer);
|
||||
bool has_instantiated_object = false;
|
||||
bool has_uninstantiated_object = false;
|
||||
for (Object *obj : ctx.mapping.imported_objects) {
|
||||
Base *base = BKE_view_layer_base_find(view_layer, obj);
|
||||
if (!base) {
|
||||
/* Object not instantiated in current viewlayer. */
|
||||
has_uninstantiated_object = true;
|
||||
continue;
|
||||
}
|
||||
has_instantiated_object = true;
|
||||
BKE_view_layer_base_select_and_set_active(view_layer, base);
|
||||
|
||||
int flags = ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
|
||||
ID_RECALC_BASE_FLAGS;
|
||||
DEG_id_tag_update_ex(bmain, &obj->id, flags);
|
||||
}
|
||||
|
||||
if (has_instantiated_object && has_uninstantiated_object) {
|
||||
CLOG_ERROR(&LOG, "Some imported objects were not instantiated, while others were");
|
||||
}
|
||||
|
||||
DEG_id_tag_update(&lc->collection->id, ID_RECALC_SYNC_TO_EVAL);
|
||||
|
||||
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
|
||||
DEG_relations_tag_update(bmain);
|
||||
}
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
26
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.hh
Normal file
26
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.hh
Normal file
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct FBXImportParams;
|
||||
struct Main;
|
||||
struct Scene;
|
||||
struct ViewLayer;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void importer_main(Main *bmain,
|
||||
Scene *scene,
|
||||
ViewLayer *view_layer,
|
||||
const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
628
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_anim.cc
Normal file
628
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_anim.cc
Normal file
@@ -0,0 +1,628 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_animdata.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_fcurve.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_object_types.hh"
|
||||
|
||||
#include "BLI_linear_allocator.hh"
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_math_axis_angle.hh"
|
||||
#include "BLI_math_quaternion.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_vector.hh"
|
||||
#include "BLI_vector_set.hh"
|
||||
|
||||
#include "DNA_key_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "IO_validate.hh"
|
||||
|
||||
#include "fbx_import_anim.hh"
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
static CLG_LogRef LOG = {"io.fbx"};
|
||||
|
||||
static FCurve *create_fcurve(animrig::Channelbag &channelbag,
|
||||
const animrig::FCurveDescriptor &descriptor,
|
||||
int64_t key_count)
|
||||
{
|
||||
if (!validate::size_fits_in_int(key_count)) {
|
||||
CLOG_WARN(&LOG, "Animation curve too large to import, exceeds max int size");
|
||||
key_count = 0;
|
||||
}
|
||||
FCurve &cu = channelbag.fcurve_ensure(nullptr, descriptor);
|
||||
BKE_fcurve_bezt_resize(cu, key_count);
|
||||
return &cu;
|
||||
}
|
||||
|
||||
static void set_curve_sample(FCurve *curve, int64_t key_index, float time, float value)
|
||||
{
|
||||
BLI_assert(key_index >= 0 && key_index < curve->totvert);
|
||||
BezTriple &bez = curve->bezt[key_index];
|
||||
bez.vec[1][0] = time;
|
||||
bez.vec[1][1] = value;
|
||||
bez.ipo = BEZT_IPO_LIN;
|
||||
bez.f1 = bez.f2 = bez.f3 = BEZT_FLAG_SELECT;
|
||||
bez.h1 = bez.h2 = HD_AUTO_ANIM;
|
||||
}
|
||||
|
||||
struct ElementAnimations {
|
||||
const ufbx_element *fbx_elem = nullptr;
|
||||
ID *target_id = nullptr;
|
||||
eRotationModes object_rotmode = ROT_MODE_QUAT;
|
||||
int64_t order = 0;
|
||||
const ufbx_anim_prop *prop_position = nullptr;
|
||||
const ufbx_anim_prop *prop_rotation = nullptr;
|
||||
const ufbx_anim_prop *prop_scale = nullptr;
|
||||
const ufbx_anim_prop *prop_blend_shape = nullptr;
|
||||
const ufbx_anim_prop *prop_focal_length = nullptr;
|
||||
const ufbx_anim_prop *prop_focus_dist = nullptr;
|
||||
const ufbx_anim_prop *prop_mat_diffuse = nullptr;
|
||||
};
|
||||
|
||||
static Vector<ElementAnimations> gather_animated_properties(const FbxElementMapping &mapping,
|
||||
const ufbx_anim_layer &flayer)
|
||||
{
|
||||
int64_t order = 0;
|
||||
Map<const ufbx_element *, ElementAnimations> elem_map;
|
||||
for (const ufbx_anim_prop &fprop : flayer.anim_props) {
|
||||
if (fprop.anim_value->curves[0] == nullptr && fprop.anim_value->curves[1] == nullptr &&
|
||||
fprop.anim_value->curves[2] == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bool supported_prop = false;
|
||||
//@TODO: "Visibility"?
|
||||
const bool is_position = STREQ(fprop.prop_name.data, "Lcl Translation");
|
||||
const bool is_rotation = STREQ(fprop.prop_name.data, "Lcl Rotation");
|
||||
const bool is_scale = STREQ(fprop.prop_name.data, "Lcl Scaling");
|
||||
const bool is_blend_shape = STREQ(fprop.prop_name.data, "DeformPercent");
|
||||
const bool is_focal_length = STREQ(fprop.prop_name.data, "FocalLength");
|
||||
const bool is_focus_dist = STREQ(fprop.prop_name.data, "FocusDistance");
|
||||
const bool is_diffuse = STREQ(fprop.prop_name.data, "DiffuseColor");
|
||||
if (is_position || is_rotation || is_scale || is_blend_shape || is_focal_length ||
|
||||
is_focus_dist || is_diffuse)
|
||||
{
|
||||
supported_prop = true;
|
||||
}
|
||||
|
||||
if (!supported_prop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool is_anim_camera = is_focal_length || is_focus_dist;
|
||||
const bool is_anim_mat = is_diffuse;
|
||||
|
||||
ID *target_id = nullptr;
|
||||
eRotationModes object_rotmode = ROT_MODE_QUAT;
|
||||
|
||||
if (is_blend_shape) {
|
||||
/* Animating blend shape weight. */
|
||||
Key *target_key = mapping.el_to_shape_key.lookup_default(fprop.element, nullptr);
|
||||
if (target_key != nullptr) {
|
||||
target_id = &target_key->id;
|
||||
}
|
||||
}
|
||||
else if (is_anim_camera) {
|
||||
/* Animating camera property. */
|
||||
if (fprop.element->instances.count > 0) {
|
||||
Object *obj = mapping.el_to_object.lookup_default(&fprop.element->instances[0]->element,
|
||||
nullptr);
|
||||
if (obj != nullptr && obj->type == OB_CAMERA) {
|
||||
target_id = (ID *)obj->data;
|
||||
object_rotmode = static_cast<eRotationModes>(obj->rotmode);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (is_anim_mat) {
|
||||
/* Animating material property. */
|
||||
Material *mat = mapping.mat_to_material.lookup_default((ufbx_material *)fprop.element,
|
||||
nullptr);
|
||||
if (mat != nullptr) {
|
||||
target_id = (ID *)mat;
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Animating Bone/Armature/Object property. */
|
||||
const ufbx_node *fnode = ufbx_as_node(fprop.element);
|
||||
Object *obj = nullptr;
|
||||
if (fnode) {
|
||||
obj = mapping.bone_to_armature.lookup_default(fnode, nullptr);
|
||||
}
|
||||
if (obj == nullptr) {
|
||||
obj = mapping.el_to_object.lookup_default(fprop.element, nullptr);
|
||||
}
|
||||
if (obj == nullptr) {
|
||||
continue;
|
||||
}
|
||||
/* Ignore animation of rigged meshes (very hard to handle; matches behavior of python fbx
|
||||
* importer). */
|
||||
if (obj->type == OB_MESH && obj->parent && obj->parent->type == OB_ARMATURE) {
|
||||
continue;
|
||||
}
|
||||
target_id = &obj->id;
|
||||
object_rotmode = static_cast<eRotationModes>(obj->rotmode);
|
||||
}
|
||||
|
||||
if (target_id == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ElementAnimations &anims = elem_map.lookup_or_add_default(fprop.element);
|
||||
anims.fbx_elem = fprop.element;
|
||||
anims.order = order++;
|
||||
anims.target_id = target_id;
|
||||
anims.object_rotmode = object_rotmode;
|
||||
|
||||
if (is_position) {
|
||||
anims.prop_position = &fprop;
|
||||
}
|
||||
if (is_rotation) {
|
||||
anims.prop_rotation = &fprop;
|
||||
}
|
||||
if (is_scale) {
|
||||
anims.prop_scale = &fprop;
|
||||
}
|
||||
if (is_blend_shape) {
|
||||
anims.prop_blend_shape = &fprop;
|
||||
}
|
||||
if (is_focal_length) {
|
||||
anims.prop_focal_length = &fprop;
|
||||
}
|
||||
if (is_focus_dist) {
|
||||
anims.prop_focus_dist = &fprop;
|
||||
}
|
||||
if (is_diffuse) {
|
||||
anims.prop_mat_diffuse = &fprop;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sort returned result in the original fbx file order. */
|
||||
Vector<ElementAnimations> animations(elem_map.values().begin(), elem_map.values().end());
|
||||
std::ranges::sort(animations, [](const ElementAnimations &a, const ElementAnimations &b) {
|
||||
return a.order < b.order;
|
||||
});
|
||||
return animations;
|
||||
}
|
||||
|
||||
static void finalize_curve(FCurve *cu)
|
||||
{
|
||||
if (cu != nullptr) {
|
||||
BKE_fcurve_handles_recalc(*cu);
|
||||
}
|
||||
}
|
||||
|
||||
static void create_transform_curve_desc(const FbxElementMapping &mapping,
|
||||
const ElementAnimations &anim,
|
||||
LinearAllocator<> &curve_name_alloc,
|
||||
Vector<animrig::FCurveDescriptor> &r_curve_desc)
|
||||
{
|
||||
/* For animated bones, prepend bone path to animation curve path. */
|
||||
std::string rna_prefix;
|
||||
std::string group_name_str = get_fbx_name(anim.fbx_elem->name);
|
||||
const ufbx_node *fnode = ufbx_as_node(anim.fbx_elem);
|
||||
const bool is_bone = mapping.node_is_blender_bone.contains(fnode);
|
||||
if (is_bone) {
|
||||
group_name_str = mapping.node_to_name.lookup_default(fnode, "");
|
||||
rna_prefix = std::string("pose.bones[\"") + group_name_str + "\"].";
|
||||
}
|
||||
|
||||
StringRefNull group_name = curve_name_alloc.copy_string(group_name_str);
|
||||
|
||||
StringRefNull rna_position = curve_name_alloc.copy_string(rna_prefix + "location");
|
||||
|
||||
StringRefNull rna_rotation;
|
||||
int rot_channels = 3;
|
||||
/* Bones are created with quaternion rotation. */
|
||||
eRotationModes rot_mode = is_bone ? ROT_MODE_QUAT : anim.object_rotmode;
|
||||
switch (rot_mode) {
|
||||
case ROT_MODE_QUAT:
|
||||
rna_rotation = curve_name_alloc.copy_string(rna_prefix + "rotation_quaternion");
|
||||
rot_channels = 4;
|
||||
break;
|
||||
case ROT_MODE_AXISANGLE:
|
||||
rna_rotation = curve_name_alloc.copy_string(rna_prefix + "rotation_axis_angle");
|
||||
rot_channels = 4;
|
||||
break;
|
||||
default:
|
||||
rna_rotation = curve_name_alloc.copy_string(rna_prefix + "rotation_euler");
|
||||
rot_channels = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
StringRefNull rna_scale = curve_name_alloc.copy_string(rna_prefix + "scale");
|
||||
|
||||
/* Fill the f-curve descriptors. */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
r_curve_desc.append({rna_position, i, {}, {}, group_name});
|
||||
}
|
||||
for (int i = 0; i < rot_channels; i++) {
|
||||
r_curve_desc.append({rna_rotation, i, {}, {}, group_name});
|
||||
}
|
||||
for (int i = 0; i < 3; i++) {
|
||||
r_curve_desc.append({rna_scale, i, {}, {}, group_name});
|
||||
}
|
||||
}
|
||||
|
||||
static void create_transform_curve_data(const FbxElementMapping &mapping,
|
||||
const ufbx_anim *fbx_anim,
|
||||
const ElementAnimations &anim,
|
||||
const double fps,
|
||||
const float anim_offset,
|
||||
FCurve **curves)
|
||||
{
|
||||
const ufbx_node *fnode = ufbx_as_node(anim.fbx_elem);
|
||||
ufbx_matrix bone_xform = ufbx_identity_matrix;
|
||||
const bool is_bone = mapping.node_is_blender_bone.contains(fnode);
|
||||
if (is_bone) {
|
||||
/* Bone transform curves need to be transformed to the bind transform
|
||||
* in joint-local space:
|
||||
* - Calculate local space bind matrix: inv(parent_bind) * bind
|
||||
* - Invert the result; this will be used to transform loc/rot/scale curves. */
|
||||
|
||||
const bool bone_at_scene_root = fnode->node_depth <= 1;
|
||||
ufbx_matrix world_to_arm = ufbx_identity_matrix;
|
||||
if (!bone_at_scene_root) {
|
||||
Object *arm_obj = mapping.bone_to_armature.lookup_default(fnode, nullptr);
|
||||
if (arm_obj != nullptr) {
|
||||
world_to_arm = mapping.armature_world_to_arm_pose_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
}
|
||||
}
|
||||
|
||||
bone_xform = mapping.calc_local_bind_matrix(fnode, world_to_arm);
|
||||
bone_xform = ufbx_matrix_invert(&bone_xform);
|
||||
}
|
||||
|
||||
int rot_channels = 3;
|
||||
/* Bones are created with quaternion rotation. */
|
||||
eRotationModes rot_mode = is_bone ? ROT_MODE_QUAT : anim.object_rotmode;
|
||||
switch (rot_mode) {
|
||||
case ROT_MODE_QUAT:
|
||||
rot_channels = 4;
|
||||
break;
|
||||
case ROT_MODE_AXISANGLE:
|
||||
rot_channels = 4;
|
||||
break;
|
||||
default:
|
||||
rot_channels = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Note: Python importer was always creating all pos/rot/scale curves: "due to all FBX
|
||||
* transform magic, we need to add curves for whole loc/rot/scale in any case".
|
||||
*
|
||||
* Also, we create a full transform keyframe at any point where input pos/rot/scale curves have
|
||||
* a keyframe. It should not be needed if we fully imported curves with all their proper
|
||||
* handles, but again currently this is to match Python importer behavior. */
|
||||
const ufbx_anim_curve *input_curves[9] = {};
|
||||
if (anim.prop_position) {
|
||||
input_curves[0] = anim.prop_position->anim_value->curves[0];
|
||||
input_curves[1] = anim.prop_position->anim_value->curves[1];
|
||||
input_curves[2] = anim.prop_position->anim_value->curves[2];
|
||||
}
|
||||
if (anim.prop_rotation) {
|
||||
input_curves[3] = anim.prop_rotation->anim_value->curves[0];
|
||||
input_curves[4] = anim.prop_rotation->anim_value->curves[1];
|
||||
input_curves[5] = anim.prop_rotation->anim_value->curves[2];
|
||||
}
|
||||
if (anim.prop_scale) {
|
||||
input_curves[6] = anim.prop_scale->anim_value->curves[0];
|
||||
input_curves[7] = anim.prop_scale->anim_value->curves[1];
|
||||
input_curves[8] = anim.prop_scale->anim_value->curves[2];
|
||||
}
|
||||
|
||||
/* Figure out timestamps of where any of input curves have a keyframe. */
|
||||
Set<double> unique_key_times;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (input_curves[i] != nullptr) {
|
||||
for (const ufbx_keyframe &key : input_curves[i]->keyframes) {
|
||||
if (key.interpolation == UFBX_INTERPOLATION_CUBIC) {
|
||||
/* Hack: force cubic keyframes to be linear, to match Python importer behavior. */
|
||||
const_cast<ufbx_keyframe &>(key).interpolation = UFBX_INTERPOLATION_LINEAR;
|
||||
}
|
||||
unique_key_times.add(key.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vector<double> sorted_key_times(unique_key_times.begin(), unique_key_times.end());
|
||||
std::ranges::sort(sorted_key_times);
|
||||
|
||||
int64_t pos_index = 0;
|
||||
int64_t rot_index = pos_index + 3;
|
||||
int64_t scale_index = rot_index + rot_channels;
|
||||
int64_t tot_curves = scale_index + 3;
|
||||
int64_t key_count = sorted_key_times.size();
|
||||
if (!validate::size_fits_in_int(key_count)) {
|
||||
CLOG_WARN(&LOG, "Animation curve too large to import, exceeds max int size");
|
||||
key_count = 0;
|
||||
}
|
||||
for (int64_t i = 0; i < tot_curves; i++) {
|
||||
BLI_assert_msg(curves[i], "fbx: animation curve was not created successfully");
|
||||
if (curves[i]) {
|
||||
BKE_fcurve_bezt_resize(*curves[i], key_count);
|
||||
}
|
||||
}
|
||||
|
||||
/* Evaluate transforms at all the key times. */
|
||||
math::Quaternion quat_prev = math::Quaternion::identity();
|
||||
for (int64_t i = 0; i < key_count; i++) {
|
||||
double t = sorted_key_times[i];
|
||||
float tf = float(t * fps + anim_offset);
|
||||
ufbx_transform xform = ufbx_evaluate_transform(fbx_anim, fnode, t);
|
||||
|
||||
if (is_bone) {
|
||||
ufbx_matrix matrix = calc_bone_pose_matrix(xform, *fnode, bone_xform);
|
||||
xform = ufbx_matrix_to_transform(&matrix);
|
||||
}
|
||||
|
||||
set_curve_sample(curves[pos_index + 0], i, tf, float(xform.translation.x));
|
||||
set_curve_sample(curves[pos_index + 1], i, tf, float(xform.translation.y));
|
||||
set_curve_sample(curves[pos_index + 2], i, tf, float(xform.translation.z));
|
||||
|
||||
math::Quaternion quat(xform.rotation.w, xform.rotation.x, xform.rotation.y, xform.rotation.z);
|
||||
switch (rot_mode) {
|
||||
case ROT_MODE_QUAT:
|
||||
/* Ensure shortest interpolation path between consecutive quaternions. */
|
||||
if (i != 0 && math::dot(quat, quat_prev) < 0.0f) {
|
||||
quat = -quat;
|
||||
}
|
||||
quat_prev = quat;
|
||||
set_curve_sample(curves[rot_index + 0], i, tf, quat.w);
|
||||
set_curve_sample(curves[rot_index + 1], i, tf, quat.x);
|
||||
set_curve_sample(curves[rot_index + 2], i, tf, quat.y);
|
||||
set_curve_sample(curves[rot_index + 3], i, tf, quat.z);
|
||||
break;
|
||||
case ROT_MODE_AXISANGLE: {
|
||||
const math::AxisAngle axis_angle = math::to_axis_angle(quat);
|
||||
set_curve_sample(curves[rot_index + 0], i, tf, axis_angle.angle().radian());
|
||||
set_curve_sample(curves[rot_index + 1], i, tf, axis_angle.axis().x);
|
||||
set_curve_sample(curves[rot_index + 2], i, tf, axis_angle.axis().y);
|
||||
set_curve_sample(curves[rot_index + 3], i, tf, axis_angle.axis().z);
|
||||
} break;
|
||||
default: {
|
||||
math::EulerXYZ euler = math::to_euler(quat);
|
||||
set_curve_sample(curves[rot_index + 0], i, tf, euler.x().radian());
|
||||
set_curve_sample(curves[rot_index + 1], i, tf, euler.y().radian());
|
||||
set_curve_sample(curves[rot_index + 2], i, tf, euler.z().radian());
|
||||
} break;
|
||||
}
|
||||
|
||||
set_curve_sample(curves[scale_index + 0], i, tf, float(xform.scale.x));
|
||||
set_curve_sample(curves[scale_index + 1], i, tf, float(xform.scale.y));
|
||||
set_curve_sample(curves[scale_index + 2], i, tf, float(xform.scale.z));
|
||||
}
|
||||
}
|
||||
|
||||
static void create_camera_curves(const ufbx_metadata &metadata,
|
||||
const ElementAnimations &anim,
|
||||
animrig::Channelbag &channelbag,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
if (anim.target_id == nullptr || GS(anim.target_id->name) != ID_CA) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (anim.prop_focal_length != nullptr) {
|
||||
const ufbx_anim_curve *input_curve = anim.prop_focal_length->anim_value->curves[0];
|
||||
FCurve *curve = create_fcurve(channelbag, {"lens", 0}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
float tf = float(fkey.time * fps + anim_offset);
|
||||
float val = float(fkey.value);
|
||||
set_curve_sample(curve, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve);
|
||||
}
|
||||
|
||||
if (anim.prop_focus_dist != nullptr) {
|
||||
const ufbx_anim_curve *input_curve = anim.prop_focus_dist->anim_value->curves[0];
|
||||
FCurve *curve = create_fcurve(
|
||||
channelbag, {"dof.focus_distance", 0}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
float tf = float(fkey.time * fps + anim_offset);
|
||||
/* Animation curves containing camera focus distance have values multiplied by 1000.0 */
|
||||
float val = float(fkey.value / 1000.0 * metadata.geometry_scale * metadata.root_scale);
|
||||
set_curve_sample(curve, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve);
|
||||
}
|
||||
}
|
||||
|
||||
static void create_material_curves(const ElementAnimations &anim,
|
||||
bAction *action,
|
||||
animrig::Channelbag &channelbag,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
if (anim.target_id == nullptr || GS(anim.target_id->name) != ID_MA) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *rna_path_1 = "diffuse_color";
|
||||
const char *rna_path_2 = "nodes[\"Principled BSDF\"].inputs[0].default_value";
|
||||
|
||||
/* Also create animation curves for the node tree diffuse color input. */
|
||||
Material *target_mat = id_cast<Material *>(anim.target_id);
|
||||
ID *target_ntree = reinterpret_cast<ID *>(target_mat->nodetree);
|
||||
animrig::Action &act = action->wrap();
|
||||
const animrig::Slot *slot = animrig::assign_action_ensure_slot_for_keying(act, *target_ntree);
|
||||
BLI_assert(slot != nullptr);
|
||||
UNUSED_VARS_NDEBUG(slot);
|
||||
animrig::Channelbag &chbag_node = animrig::action_channelbag_ensure(*action, *target_ntree);
|
||||
|
||||
if (anim.prop_mat_diffuse != nullptr) {
|
||||
for (int ch = 0; ch < 3; ch++) {
|
||||
const ufbx_anim_curve *input_curve = anim.prop_mat_diffuse->anim_value->curves[ch];
|
||||
FCurve *curve_1 = create_fcurve(channelbag, {rna_path_1, ch}, input_curve->keyframes.count);
|
||||
FCurve *curve_2 = create_fcurve(chbag_node, {rna_path_2, ch}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve_1->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
float tf = float(fkey.time * fps + anim_offset);
|
||||
float val = float(fkey.value);
|
||||
set_curve_sample(curve_1, i, tf, val);
|
||||
set_curve_sample(curve_2, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve_1);
|
||||
finalize_curve(curve_2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void create_blend_shape_curves(const ElementAnimations &anim,
|
||||
animrig::Channelbag &channelbag,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
const ufbx_blend_channel *fchan = ufbx_as_blend_channel(anim.prop_blend_shape->element);
|
||||
BLI_assert(fchan != nullptr);
|
||||
std::string rna_path = std::string("key_blocks[\"") + fchan->target_shape->name.data +
|
||||
"\"].value";
|
||||
const ufbx_anim_curve *input_curve = anim.prop_blend_shape->anim_value->curves[0];
|
||||
FCurve *curve = create_fcurve(channelbag, {rna_path, 0}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
double t = fkey.time;
|
||||
float tf = float(t * fps + anim_offset);
|
||||
float val = float(fkey.value / 100.0); /* FBX shape weights are 0..100 range. */
|
||||
set_curve_sample(curve, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve);
|
||||
}
|
||||
|
||||
void import_animations(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
const FbxElementMapping &mapping,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
/* Note: mixing is completely ignored for now, each layer results in an independent set of
|
||||
* actions. */
|
||||
for (const ufbx_anim_stack *fstack : fbx.anim_stacks) {
|
||||
for (const ufbx_anim_layer *flayer : fstack->layers) {
|
||||
Vector<ElementAnimations> animations = gather_animated_properties(mapping, *flayer);
|
||||
if (animations.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Create action for this layer. */
|
||||
std::string action_name = fstack->name.data;
|
||||
if (!STREQ(fstack->name.data, flayer->name.data) && fstack->layers.count != 1) {
|
||||
action_name += '|';
|
||||
action_name += flayer->name.data;
|
||||
}
|
||||
animrig::Action &action = animrig::action_add(bmain, action_name);
|
||||
id_fake_user_set(&action.id);
|
||||
action.layer_keystrip_ensure();
|
||||
animrig::StripKeyframeData &strip_data =
|
||||
action.layer(0)->strip(0)->data<animrig::StripKeyframeData>(action);
|
||||
|
||||
/* Figure out the set of IDs that are animated. We want to preserve the order
|
||||
* of this set to match order of animations inside the FBX file. */
|
||||
VectorSet<ID *> animated_ids;
|
||||
Map<ID *, Vector<const ElementAnimations *>> id_to_anims;
|
||||
for (const ElementAnimations &anim : animations) {
|
||||
animated_ids.add(anim.target_id);
|
||||
Vector<const ElementAnimations *> &anims = id_to_anims.lookup_or_add_default(
|
||||
anim.target_id);
|
||||
anims.append(&anim);
|
||||
}
|
||||
|
||||
/* Create action slots for each animated ID. */
|
||||
for (ID *id : animated_ids) {
|
||||
/* Create a slot for this ID. */
|
||||
BLI_assert(id != nullptr);
|
||||
const std::string slot_name = id->name;
|
||||
animrig::Slot &slot = action.slot_add_for_id_type(GS(id->name));
|
||||
action.slot_identifier_define(slot, slot_name);
|
||||
|
||||
/* Assign this action & slot to ID. */
|
||||
const AnimData *adt = BKE_animdata_ensure_id(id);
|
||||
BLI_assert_msg(adt != nullptr, "fbx: could not create animation data for an ID");
|
||||
if (adt->action == nullptr) {
|
||||
bool ok = animrig::assign_action(&action, *id);
|
||||
BLI_assert_msg(ok, "fbx: could not assign action to ID");
|
||||
UNUSED_VARS_NDEBUG(ok);
|
||||
}
|
||||
if (adt->slot_handle == animrig::Slot::unassigned) {
|
||||
animrig::ActionSlotAssignmentResult res = animrig::assign_action_slot(&slot, *id);
|
||||
BLI_assert_msg(res == animrig::ActionSlotAssignmentResult::OK,
|
||||
"fbx: failed to assign slot to ID");
|
||||
UNUSED_VARS_NDEBUG(res);
|
||||
}
|
||||
animrig::Channelbag &channelbag = strip_data.channelbag_for_slot_ensure(slot);
|
||||
|
||||
/* Create animation curves for this ID. */
|
||||
Vector<const ElementAnimations *> id_anims = id_to_anims.lookup(id);
|
||||
/* Batch create the transform curves: creating them one by one is not very fast,
|
||||
* especially for armatures where many bones often are animated. So first create
|
||||
* their descriptors, then create the f-curves in one step, and finally fill their data. */
|
||||
Vector<animrig::FCurveDescriptor> curve_desc;
|
||||
Vector<int64_t> anim_transform_curve_index(id_anims.size());
|
||||
LinearAllocator name_alloc;
|
||||
for (const int64_t index : id_anims.index_range()) {
|
||||
const ElementAnimations *anim = id_anims[index];
|
||||
if (anim->prop_position || anim->prop_rotation || anim->prop_scale) {
|
||||
anim_transform_curve_index[index] = curve_desc.size();
|
||||
create_transform_curve_desc(mapping, *anim, name_alloc, curve_desc);
|
||||
}
|
||||
else {
|
||||
anim_transform_curve_index[index] = -1;
|
||||
}
|
||||
}
|
||||
Vector<FCurve *> transform_curves;
|
||||
if (!curve_desc.is_empty()) {
|
||||
transform_curves = channelbag.fcurve_create_many(nullptr, curve_desc.as_span());
|
||||
}
|
||||
|
||||
for (const int64_t index : id_anims.index_range()) {
|
||||
const ElementAnimations *anim = id_anims[index];
|
||||
if (anim->prop_position || anim->prop_rotation || anim->prop_scale) {
|
||||
create_transform_curve_data(mapping,
|
||||
flayer->anim,
|
||||
*anim,
|
||||
fps,
|
||||
anim_offset,
|
||||
transform_curves.data() +
|
||||
anim_transform_curve_index[index]);
|
||||
}
|
||||
if (anim->prop_focal_length || anim->prop_focus_dist) {
|
||||
create_camera_curves(fbx.metadata, *anim, channelbag, fps, anim_offset);
|
||||
}
|
||||
if (anim->prop_mat_diffuse) {
|
||||
create_material_curves(*anim, &action, channelbag, fps, anim_offset);
|
||||
}
|
||||
if (anim->prop_blend_shape) {
|
||||
create_blend_shape_curves(*anim, channelbag, fps, anim_offset);
|
||||
}
|
||||
}
|
||||
|
||||
for (FCurve *curve : transform_curves) {
|
||||
finalize_curve(curve);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Main;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void import_animations(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
const FbxElementMapping &mapping,
|
||||
const double fps,
|
||||
const float anim_offset);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,433 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_armature.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_object.hh"
|
||||
|
||||
#include "BLI_math_vector.hh"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "ED_armature.hh"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
|
||||
#include "fbx_import_armature.hh"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
struct ArmatureImportContext {
|
||||
Main &bmain;
|
||||
const ufbx_scene &fbx;
|
||||
const FBXImportParams ¶ms;
|
||||
FbxElementMapping &mapping;
|
||||
|
||||
ArmatureImportContext(Main &main,
|
||||
const ufbx_scene &fbx,
|
||||
const FBXImportParams ¶ms,
|
||||
FbxElementMapping &mapping)
|
||||
: bmain(main), fbx(fbx), params(params), mapping(mapping)
|
||||
{
|
||||
}
|
||||
|
||||
Object *create_armature_for_node(const ufbx_node *node);
|
||||
void create_armature_bones(const ufbx_node *node,
|
||||
Object *arm_obj,
|
||||
const Set<const ufbx_node *> &bone_nodes,
|
||||
EditBone *parent_bone,
|
||||
const ufbx_matrix &parent_mtx,
|
||||
const ufbx_matrix &world_to_arm,
|
||||
const float parent_bone_size);
|
||||
void find_armatures(const ufbx_node *node);
|
||||
void calc_bone_bind_matrices();
|
||||
};
|
||||
|
||||
Object *ArmatureImportContext::create_armature_for_node(const ufbx_node *node)
|
||||
{
|
||||
BLI_assert_msg(node != nullptr, "fbx: node for armature creation should not be null");
|
||||
|
||||
const char *arm_name = get_fbx_name(node->name, "Armature");
|
||||
const char *obj_name = get_fbx_name(node->name, "Armature");
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "create ARMATURE %s\n", arm_name);
|
||||
#endif
|
||||
|
||||
bArmature *arm = BKE_armature_add(&this->bmain, arm_name);
|
||||
Object *obj = BKE_object_add_only_object(&this->bmain, OB_ARMATURE, obj_name);
|
||||
obj->dtx |= OB_DRAW_IN_FRONT;
|
||||
obj->data = id_cast<ID *>(arm);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
if (!node->is_root) {
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
|
||||
/* Record world to fbx node matrix for the armature object. */
|
||||
ufbx_matrix world_to_arm = ufbx_matrix_invert(&node->node_to_world);
|
||||
this->mapping.armature_world_to_arm_node_matrix.add(obj, world_to_arm);
|
||||
|
||||
/* Record world to posed root node matrix. */
|
||||
if (node->bind_pose && node->bind_pose->is_bind_pose) {
|
||||
for (const ufbx_bone_pose &pose : node->bind_pose->bone_poses) {
|
||||
if (pose.bone_node == node) {
|
||||
world_to_arm = ufbx_matrix_invert(&pose.bone_to_world);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->mapping.armature_world_to_arm_pose_matrix.add(obj, world_to_arm);
|
||||
}
|
||||
else {
|
||||
/* For armatures created at root, make them have the same rotation/scale
|
||||
* as done by ufbx for all regular nodes. */
|
||||
ufbx_matrix_to_obj(this->mapping.global_conv_matrix, obj);
|
||||
ufbx_matrix world_to_arm = ufbx_matrix_invert(&this->mapping.global_conv_matrix);
|
||||
this->mapping.armature_world_to_arm_pose_matrix.add(obj, world_to_arm);
|
||||
this->mapping.armature_world_to_arm_node_matrix.add(obj, world_to_arm);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
Object *arm_obj,
|
||||
const Set<const ufbx_node *> &bone_nodes,
|
||||
EditBone *parent_bone,
|
||||
const ufbx_matrix &parent_mtx,
|
||||
const ufbx_matrix &world_to_arm,
|
||||
const float parent_bone_size)
|
||||
{
|
||||
BLI_assert(node != nullptr && !node->is_root);
|
||||
bArmature *arm = id_cast<bArmature *>(arm_obj->data);
|
||||
|
||||
/* Create an EditBone. */
|
||||
std::string name;
|
||||
if (node->is_geometry_transform_helper) {
|
||||
/* Name geometry transform adjustment helpers with parent name and _GeomAdjust suffix. */
|
||||
name = get_fbx_name(node->parent->name, "Bone") + std::string("_GeomAdjust");
|
||||
}
|
||||
else {
|
||||
name = get_fbx_name(node->name, "Bone");
|
||||
}
|
||||
EditBone *bone = ED_armature_ebone_add(arm, name.c_str());
|
||||
this->mapping.node_to_name.add(node, bone->name);
|
||||
this->mapping.node_is_blender_bone.add(node);
|
||||
this->mapping.bone_to_armature.add(node, arm_obj);
|
||||
bone->flag |= BONE_SELECTED;
|
||||
bone->parent = parent_bone;
|
||||
if (node->inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) {
|
||||
bone->inherit_scale_mode = BONE_INHERIT_SCALE_NONE;
|
||||
}
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file,
|
||||
"create BONE %s (parent %s) parent_mtx:\n",
|
||||
node->name.data,
|
||||
parent_bone ? parent_bone->name : "");
|
||||
print_matrix(parent_mtx);
|
||||
#endif
|
||||
|
||||
ufbx_matrix bone_mtx = this->mapping.get_node_bind_matrix(node);
|
||||
bone_mtx = ufbx_matrix_mul(&world_to_arm, &bone_mtx);
|
||||
bone_mtx.cols[0] = ufbx_vec3_normalize(bone_mtx.cols[0]);
|
||||
bone_mtx.cols[1] = ufbx_vec3_normalize(bone_mtx.cols[1]);
|
||||
bone_mtx.cols[2] = ufbx_vec3_normalize(bone_mtx.cols[2]);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, " bone_mtx:\n");
|
||||
print_matrix(bone_mtx);
|
||||
#endif
|
||||
|
||||
/* Calculate bone tail position. */
|
||||
float bone_size = 0.0f;
|
||||
int child_bone_count = 0;
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (!bone_nodes.contains(fchild)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Estimate child position from local transform, but if the child
|
||||
* is skinned/posed then use the posed transform instead. */
|
||||
ufbx_vec3 pos = fchild->local_transform.translation;
|
||||
if (this->mapping.bone_to_bind_matrix.contains(fchild)) {
|
||||
ufbx_matrix local_mtx = this->mapping.calc_local_bind_matrix(fchild, world_to_arm);
|
||||
pos = local_mtx.cols[3];
|
||||
}
|
||||
bone_size += math::length(float3(pos.x, pos.y, pos.z));
|
||||
child_bone_count++;
|
||||
}
|
||||
if (child_bone_count > 0) {
|
||||
bone_size /= child_bone_count;
|
||||
}
|
||||
else {
|
||||
/* This is leaf bone, set length to parent bone length. */
|
||||
bone_size = parent_bone_size;
|
||||
/* If we do not have actual pose/skin matrix for this bone, apply local transform onto parent
|
||||
* matrix. */
|
||||
if (!this->mapping.bone_to_bind_matrix.contains(node)) {
|
||||
ufbx_matrix offset_mtx = ufbx_transform_to_matrix(&node->local_transform);
|
||||
bone_mtx = ufbx_matrix_mul(&parent_mtx, &offset_mtx);
|
||||
bone_mtx.cols[0] = ufbx_vec3_normalize(bone_mtx.cols[0]);
|
||||
bone_mtx.cols[1] = ufbx_vec3_normalize(bone_mtx.cols[1]);
|
||||
bone_mtx.cols[2] = ufbx_vec3_normalize(bone_mtx.cols[2]);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, " bone_mtx adj for non-posed bones:\n");
|
||||
print_matrix(bone_mtx);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
/* Zero length bones are automatically collapsed into their parent when you leave edit mode,
|
||||
* so enforce a minimum length. */
|
||||
bone_size = math::max(bone_size, 0.01f);
|
||||
this->mapping.bone_to_length.add(node, bone_size);
|
||||
|
||||
bone->tail[0] = 0.0f;
|
||||
bone->tail[1] = bone_size;
|
||||
bone->tail[2] = 0.0f;
|
||||
/* Set bone matrix. */
|
||||
float bone_matrix[4][4];
|
||||
matrix_to_m44(bone_mtx, bone_matrix);
|
||||
ED_armature_ebone_from_mat4(bone, bone_matrix);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file,
|
||||
" length %.3f head (%.3f %.3f %.3f) tail (%.3f %.3f %.3f)\n",
|
||||
adjf(bone_size),
|
||||
adjf(bone->head[0]),
|
||||
adjf(bone->head[1]),
|
||||
adjf(bone->head[2]),
|
||||
adjf(bone->tail[0]),
|
||||
adjf(bone->tail[1]),
|
||||
adjf(bone->tail[2]));
|
||||
#endif
|
||||
|
||||
/* Mark bone as connected to parent if head approximately in the same place as parent tail, in
|
||||
* both rest pose and current pose. */
|
||||
if (parent_bone != nullptr) {
|
||||
float3 self_head_rest(bone->head);
|
||||
float3 par_tail_rest(parent_bone->tail);
|
||||
const float connect_dist = 1.0e-4f;
|
||||
const float connect_dist_sq = connect_dist * connect_dist;
|
||||
float dist_sq_rest = math::distance_squared(self_head_rest, par_tail_rest);
|
||||
if (dist_sq_rest < connect_dist_sq) {
|
||||
/* Bones seem connected in rest pose, now check their current transforms. */
|
||||
ufbx_vec3 self_head_cur_u = node->node_to_world.cols[3];
|
||||
ufbx_vec3 par_tail;
|
||||
par_tail.x = 0;
|
||||
par_tail.y = parent_bone_size;
|
||||
par_tail.z = 0;
|
||||
ufbx_vec3 par_tail_cur_u = ufbx_transform_position(&node->parent->node_to_world, par_tail);
|
||||
float3 self_head_cur(self_head_cur_u.x, self_head_cur_u.y, self_head_cur_u.z);
|
||||
float3 par_tail_cur(par_tail_cur_u.x, par_tail_cur_u.y, par_tail_cur_u.z);
|
||||
float dist_sq_cur = math::distance_squared(self_head_cur, par_tail_cur);
|
||||
|
||||
if (dist_sq_cur < connect_dist_sq) {
|
||||
/* Connected in both cases. */
|
||||
bone->flag |= BONE_CONNECTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Recurse into child bones. */
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (!bone_nodes.contains(fchild)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool skip_child = false;
|
||||
if (this->params.ignore_leaf_bones) {
|
||||
if (node->children.count == 1 && fchild->children.count == 0 &&
|
||||
!mapping.bone_is_skinned.contains(fchild))
|
||||
{
|
||||
skip_child = true;
|
||||
/* We are skipping this bone, but still record it --
|
||||
* so that later code does not try to create an empty for it. */
|
||||
this->mapping.node_is_blender_bone.add(fchild);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip_child) {
|
||||
create_armature_bones(fchild, arm_obj, bone_nodes, bone, bone_mtx, world_to_arm, bone_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Need to create armature if we are root bone, or any child is a non-root bone. */
|
||||
static bool need_create_armature_for_node(const ufbx_node *node)
|
||||
{
|
||||
if (node->bone && node->bone->is_root) {
|
||||
return true;
|
||||
}
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (fchild->bone && !fchild->bone->is_root) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void find_bones(const ufbx_node *node, Set<const ufbx_node *> &r_bones)
|
||||
{
|
||||
if (node->bone != nullptr) {
|
||||
r_bones.add(node);
|
||||
}
|
||||
for (const ufbx_node *child : node->children) {
|
||||
find_bones(child, r_bones);
|
||||
}
|
||||
}
|
||||
|
||||
static void find_fake_bones(const ufbx_node *root_node,
|
||||
const Set<const ufbx_node *> &bones,
|
||||
Set<const ufbx_node *> &r_fake_bones)
|
||||
{
|
||||
for (const ufbx_node *bone_node : bones) {
|
||||
const ufbx_node *node = bone_node->parent;
|
||||
while (!ELEM(node, nullptr, root_node)) {
|
||||
if (node->bone == nullptr) {
|
||||
r_fake_bones.add(node);
|
||||
}
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Set<const ufbx_node *> find_all_bones(const ufbx_node *root_node)
|
||||
{
|
||||
/* Find regular FBX bones nodes anywhere under our root armature node. */
|
||||
Set<const ufbx_node *> bones;
|
||||
find_bones(root_node, bones);
|
||||
|
||||
/* There might be non-bone nodes in between, e.g. FBX structure being like:
|
||||
* BoneA -> MeshB -> BoneC -> MeshD. Blender Armature can only contain
|
||||
* bones, so in this case "MeshB" has to have a bone created for it as well.
|
||||
* "Fake bones" are any non-bone FBX nodes in between root armature node
|
||||
* and the actual bone node. */
|
||||
Set<const ufbx_node *> fake_bones;
|
||||
find_fake_bones(root_node, bones, fake_bones);
|
||||
for (const ufbx_node *b : fake_bones) {
|
||||
bones.add(b);
|
||||
}
|
||||
return bones;
|
||||
}
|
||||
|
||||
void ArmatureImportContext::find_armatures(const ufbx_node *node)
|
||||
{
|
||||
const bool needs_arm = need_create_armature_for_node(node);
|
||||
if (needs_arm) {
|
||||
/* Create armature. */
|
||||
Object *arm_obj = this->create_armature_for_node(node);
|
||||
ufbx_matrix world_to_arm = this->mapping.armature_world_to_arm_pose_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
|
||||
Set<const ufbx_node *> bone_nodes = find_all_bones(node);
|
||||
|
||||
/* Create bones in edit mode. */
|
||||
bArmature *arm = id_cast<bArmature *>(arm_obj->data);
|
||||
ED_armature_to_edit(arm);
|
||||
this->mapping.node_to_name.add(node, BKE_id_name(arm_obj->id));
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (bone_nodes.contains(fchild)) {
|
||||
create_armature_bones(
|
||||
fchild, arm_obj, bone_nodes, nullptr, ufbx_identity_matrix, world_to_arm, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ED_armature_from_edit(&this->bmain, arm);
|
||||
ED_armature_edit_free(arm);
|
||||
|
||||
/* Setup pose on the object, and custom properties on the bone pose channels. */
|
||||
for (const ufbx_node *fbone : bone_nodes) {
|
||||
if (!this->mapping.node_is_blender_bone.contains(fbone)) {
|
||||
continue; /* Blender bone was not created for it (e.g. root bone in some cases). */
|
||||
}
|
||||
bPoseChannel *pchan = BKE_pose_channel_find_name(
|
||||
arm_obj->pose, this->mapping.node_to_name.lookup_default(fbone, "").c_str());
|
||||
if (pchan == nullptr) {
|
||||
continue;
|
||||
}
|
||||
read_custom_properties(fbone->props, *pchan, this->params.props_enum_as_string);
|
||||
|
||||
/* For bones that have rest/bind information, put their current transform into
|
||||
* the current pose. */
|
||||
if (this->mapping.bone_to_bind_matrix.contains(fbone)) {
|
||||
ufbx_matrix bind_local_mtx = this->mapping.calc_local_bind_matrix(fbone, world_to_arm);
|
||||
ufbx_matrix bind_local_mtx_inv = ufbx_matrix_invert(&bind_local_mtx);
|
||||
ufbx_transform xform = fbone->local_transform;
|
||||
if (fbone->node_depth <= 1) {
|
||||
ufbx_matrix matrix = ufbx_matrix_mul(&world_to_arm, &fbone->node_to_world);
|
||||
xform = ufbx_matrix_to_transform(&matrix);
|
||||
}
|
||||
ufbx_matrix pose_mtx = calc_bone_pose_matrix(xform, *fbone, bind_local_mtx_inv);
|
||||
|
||||
float pchan_matrix[4][4];
|
||||
matrix_to_m44(pose_mtx, pchan_matrix);
|
||||
BKE_pchan_apply_mat4(pchan, pchan_matrix, false);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "set POSE matrix of %s matrix_basis:\n", fbone->name.data);
|
||||
print_matrix(pose_mtx);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Recurse into children that have not been turned into bones yet. */
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (!this->mapping.node_is_blender_bone.contains(fchild)) {
|
||||
this->find_armatures(fchild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ArmatureImportContext::calc_bone_bind_matrices()
|
||||
{
|
||||
/* Figure out bind matrices for bone nodes:
|
||||
* - Get them from "pose" objects in FBX that are marked as "bind pose",
|
||||
* - From all "skin deformer" objects in FBX; these override the ones from "poses".
|
||||
* - For all the bone nodes that do not have a matrix yet, record their world matrix
|
||||
* as bind matrix. */
|
||||
for (const ufbx_pose *fpose : this->fbx.poses) {
|
||||
if (!fpose->is_bind_pose) {
|
||||
continue;
|
||||
}
|
||||
for (const ufbx_bone_pose &bone_pose : fpose->bone_poses) {
|
||||
const ufbx_matrix &bind_matrix = bone_pose.bone_to_world;
|
||||
this->mapping.bone_to_bind_matrix.add_overwrite(bone_pose.bone_node, bind_matrix);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "bone POSE matrix %s\n", bone_pose.bone_node->name.data);
|
||||
print_matrix(bind_matrix);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
for (const ufbx_skin_deformer *fskin : this->fbx.skin_deformers) {
|
||||
for (const ufbx_skin_cluster *fbone : fskin->clusters) {
|
||||
const ufbx_matrix &bind_matrix = fbone->bind_to_world;
|
||||
this->mapping.bone_to_bind_matrix.add_overwrite(fbone->bone_node, bind_matrix);
|
||||
this->mapping.bone_is_skinned.add(fbone->bone_node);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "bone SKIN matrix %s\n", fbone->bone_node->name.data);
|
||||
print_matrix(bind_matrix);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void import_armatures(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms)
|
||||
{
|
||||
ArmatureImportContext context(bmain, fbx, params, mapping);
|
||||
context.calc_bone_bind_matrices();
|
||||
context.find_armatures(fbx.root_node);
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct FBXImportParams;
|
||||
struct Main;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void import_armatures(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,467 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_image.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_material.hh"
|
||||
#include "BKE_node_legacy_types.hh"
|
||||
#include "BKE_node_runtime.hh"
|
||||
#include "BKE_node_tree_update.hh"
|
||||
|
||||
#include "BLI_math_vector.hh"
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
|
||||
#include "DNA_material_types.h"
|
||||
|
||||
#include "NOD_shader.h"
|
||||
|
||||
#include "IMB_colormanagement.hh"
|
||||
#include "IMB_imbuf_types.hh"
|
||||
|
||||
#include "fbx_import_material.hh"
|
||||
|
||||
#include "ufbx.h"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
/* Nodes are arranged in columns by type, with manually placed x coordinates
|
||||
* based on node widths. */
|
||||
static constexpr float node_locx_texcoord = -880.0f;
|
||||
static constexpr float node_locx_mapping = -680.0f;
|
||||
static constexpr float node_locx_image = -480.0f;
|
||||
static constexpr float node_locx_normalmap = -200.0f;
|
||||
static constexpr float node_locx_bsdf = 0.0f;
|
||||
static constexpr float node_locx_output = 280.0f;
|
||||
|
||||
/* Nodes are arranged in rows; one row for each image being used. */
|
||||
static constexpr float node_locy_top = 300.0f;
|
||||
static constexpr float node_locy_step = 300.0f;
|
||||
|
||||
/* Add a node of the given type at the given location. */
|
||||
static bNode *add_node(bNodeTree *ntree, int type, float x, float y)
|
||||
{
|
||||
bNode *node = bke::node_add_static_node(nullptr, *ntree, type);
|
||||
node->location[0] = x;
|
||||
node->location[1] = y;
|
||||
return node;
|
||||
}
|
||||
|
||||
static void link_sockets(bNodeTree *ntree,
|
||||
bNode *from_node,
|
||||
const char *from_node_id,
|
||||
bNode *to_node,
|
||||
const char *to_node_id)
|
||||
{
|
||||
bNodeSocket *from_sock{bke::node_find_socket(*from_node, SOCK_OUT, UString(from_node_id))};
|
||||
bNodeSocket *to_sock{bke::node_find_socket(*to_node, SOCK_IN, UString(to_node_id))};
|
||||
BLI_assert(from_sock && to_sock);
|
||||
bke::node_add_link(*ntree, *from_node, *from_sock, *to_node, *to_sock);
|
||||
}
|
||||
|
||||
static void set_socket_float(const char *socket_id, const float value, bNode *node)
|
||||
{
|
||||
bNodeSocket *socket{bke::node_find_socket(*node, SOCK_IN, UString(socket_id))};
|
||||
BLI_assert(socket && socket->type == SOCK_FLOAT);
|
||||
bNodeSocketValueFloat *dst = socket->default_value_typed<bNodeSocketValueFloat>();
|
||||
dst->value = value;
|
||||
}
|
||||
|
||||
static void set_socket_rgb(const char *socket_id, float vr, float vg, float vb, bNode *node)
|
||||
{
|
||||
bNodeSocket *socket{bke::node_find_socket(*node, SOCK_IN, UString(socket_id))};
|
||||
BLI_assert(socket && socket->type == SOCK_RGBA);
|
||||
bNodeSocketValueRGBA *dst = socket->default_value_typed<bNodeSocketValueRGBA>();
|
||||
dst->value[0] = vr;
|
||||
dst->value[1] = vg;
|
||||
dst->value[2] = vb;
|
||||
dst->value[3] = 1.0f;
|
||||
}
|
||||
|
||||
static void set_socket_vector(const char *socket_id, float vx, float vy, float vz, bNode *node)
|
||||
{
|
||||
bNodeSocket *socket{bke::node_find_socket(*node, SOCK_IN, UString(socket_id))};
|
||||
BLI_assert(socket && socket->type == SOCK_VECTOR);
|
||||
bNodeSocketValueVector *dst = socket->default_value_typed<bNodeSocketValueVector>();
|
||||
dst->value[0] = vx;
|
||||
dst->value[1] = vy;
|
||||
dst->value[2] = vz;
|
||||
}
|
||||
|
||||
static float set_bsdf_float_param(bNode *bsdf,
|
||||
const ufbx_material_map &umap,
|
||||
const char *socket,
|
||||
float def,
|
||||
float min = 0.0f,
|
||||
float max = 1.0f,
|
||||
float multiplier = 1.0f)
|
||||
{
|
||||
if (!umap.has_value) {
|
||||
return def * multiplier;
|
||||
}
|
||||
float value = umap.value_real * multiplier;
|
||||
value = math::clamp(value, min, max);
|
||||
set_socket_float(socket, value, bsdf);
|
||||
return value;
|
||||
}
|
||||
|
||||
static float3 set_bsdf_color_param(bNode *bsdf,
|
||||
const ufbx_material_map &umap,
|
||||
const char *socket,
|
||||
float3 def,
|
||||
float3 min = float3(0.0f),
|
||||
float3 max = float3(1.0f))
|
||||
{
|
||||
if (!umap.has_value || umap.value_components < 3) {
|
||||
return def;
|
||||
}
|
||||
float3 value = float3(umap.value_vec3.x, umap.value_vec3.y, umap.value_vec3.z);
|
||||
value = math::clamp(value, min, max);
|
||||
set_socket_rgb(socket, value.x, value.y, value.z, bsdf);
|
||||
return value;
|
||||
}
|
||||
|
||||
static void set_bsdf_socket_values(bNode *bsdf, Material *mat, const ufbx_material &fmat)
|
||||
{
|
||||
float3 base_color = set_bsdf_color_param(bsdf, fmat.pbr.base_color, "Base Color", float3(0.8f));
|
||||
mat->r = base_color.x;
|
||||
mat->g = base_color.y;
|
||||
mat->b = base_color.z;
|
||||
|
||||
float roughness = set_bsdf_float_param(bsdf, fmat.pbr.roughness, "Roughness", 0.5f);
|
||||
mat->roughness = roughness;
|
||||
|
||||
float metallic = set_bsdf_float_param(bsdf, fmat.pbr.metalness, "Metallic", 0.0f);
|
||||
mat->metallic = metallic;
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.specular_ior, "IOR", 1.5f, 1.0f, 1000.0f);
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.opacity, "Alpha", 1.0f);
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.diffuse_roughness, "Diffuse Roughness", 0.0f);
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.subsurface_factor, "Subsurface Weight", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.subsurface_scale, "Subsurface Scale", 0.05f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.subsurface_anisotropy, "Subsurface Anisotropy", 0.0f);
|
||||
|
||||
if (fmat.features.specular.enabled) {
|
||||
float spec = set_bsdf_float_param(
|
||||
bsdf, fmat.pbr.specular_factor, "Specular IOR Level", 0.25f, 0.0f, 1.0f, 2.0f);
|
||||
mat->spec = spec;
|
||||
set_bsdf_color_param(bsdf, fmat.pbr.specular_color, "Specular Tint", float3(1.0f));
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.specular_anisotropy, "Anisotropic", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.specular_rotation, "Anisotropic Rotation", 0.0f);
|
||||
}
|
||||
|
||||
if (ELEM(fmat.shader_type,
|
||||
UFBX_SHADER_OSL_STANDARD_SURFACE,
|
||||
UFBX_SHADER_ARNOLD_STANDARD_SURFACE,
|
||||
UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL,
|
||||
UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH,
|
||||
UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS,
|
||||
UFBX_SHADER_GLTF_MATERIAL,
|
||||
UFBX_SHADER_BLENDER_PHONG) &&
|
||||
fmat.features.transmission.enabled)
|
||||
{
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.transmission_factor, "Transmission Weight", 0.0f);
|
||||
}
|
||||
|
||||
if (fmat.features.coat.enabled) {
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.coat_factor, "Coat Weight", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.coat_roughness, "Coat Roughness", 0.03f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.coat_ior, "Coat IOR", 1.5f, 1.0f, 4.0f);
|
||||
set_bsdf_color_param(bsdf, fmat.pbr.coat_color, "Coat Tint", float3(1.0f));
|
||||
}
|
||||
|
||||
if (fmat.features.sheen.enabled) {
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.sheen_factor, "Sheen Weight", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.sheen_roughness, "Sheen Roughness", 0.5f);
|
||||
set_bsdf_color_param(bsdf, fmat.pbr.sheen_color, "Sheen Tint", float3(1.0f));
|
||||
}
|
||||
|
||||
set_bsdf_float_param(
|
||||
bsdf, fmat.pbr.emission_factor, "Emission Strength", 0.0f, 0.0f, 1000000.0f);
|
||||
set_bsdf_color_param(bsdf,
|
||||
fmat.pbr.emission_color,
|
||||
"Emission Color",
|
||||
float3(0.0f),
|
||||
float3(0.0f),
|
||||
float3(1000000.0f));
|
||||
|
||||
set_bsdf_float_param(
|
||||
bsdf, fmat.pbr.thin_film_thickness, "Thin Film Thickness", 0.0f, 0.0f, 100000.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.thin_film_ior, "Thin Film IOR", 1.33f, 1.0f, 1000.0f);
|
||||
}
|
||||
|
||||
static Image *create_placeholder_image(Main *bmain, const std::string &path)
|
||||
{
|
||||
const float color[4] = {0, 0, 0, 1};
|
||||
const char *name = BLI_path_basename(path.c_str());
|
||||
Image *image = BKE_image_add_generated(
|
||||
bmain, 1, 1, name, 24, false, IMA_GENTYPE_BLANK, color, false, false, false);
|
||||
STRNCPY(image->filepath, path.c_str());
|
||||
|
||||
/* Ensure that we are not marked as a generated image and clear any buffers created so far. */
|
||||
image->source = IMA_SRC_FILE;
|
||||
image->type = IMA_TYPE_IMAGE;
|
||||
BKE_image_free_buffers(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
static Image *load_texture_image(Main *bmain, const std::string &file_dir, const ufbx_texture &tex)
|
||||
{
|
||||
/* Check with filename directly. */
|
||||
Image *image = BKE_image_load_exists(bmain, tex.filename.data);
|
||||
/* Try loading as a relative path. */
|
||||
if (image == nullptr) {
|
||||
std::string path = file_dir + "/" + tex.filename.data;
|
||||
image = BKE_image_load_exists(bmain, path.c_str());
|
||||
}
|
||||
/* Try loading with absolute path from FBX. */
|
||||
if (image == nullptr) {
|
||||
image = BKE_image_load_exists(bmain, tex.absolute_filename.data);
|
||||
}
|
||||
|
||||
/* If still not found, try taking progressively longer parts of the absolute path,
|
||||
* as relative to the file. */
|
||||
if (image == nullptr) {
|
||||
size_t pos = tex.absolute_filename.length;
|
||||
do {
|
||||
const char *parent_path = BLI_path_parent_dir_end(tex.absolute_filename.data, pos);
|
||||
if (parent_path == nullptr) {
|
||||
break;
|
||||
}
|
||||
char path[FILE_MAX];
|
||||
BLI_path_join(path, sizeof(path), file_dir.c_str(), parent_path);
|
||||
BLI_path_normalize(path);
|
||||
image = BKE_image_load_exists(bmain, path);
|
||||
pos = parent_path - tex.absolute_filename.data;
|
||||
} while (image == nullptr);
|
||||
}
|
||||
|
||||
/* Create dummy/placeholder image. */
|
||||
if (image == nullptr) {
|
||||
image = create_placeholder_image(bmain, tex.filename.data);
|
||||
}
|
||||
|
||||
/* Use embedded data for this image, if we haven't done that yet. */
|
||||
if (tex.content.size > 0 && (image == nullptr || !BKE_image_has_packedfile(image))) {
|
||||
BKE_image_free_buffers(image); /* Free cached placeholder images. */
|
||||
char *data_dup = MEM_new_array_uninitialized<char>(tex.content.size, __func__);
|
||||
memcpy(data_dup, tex.content.data, tex.content.size);
|
||||
BKE_image_packfiles_from_mem(nullptr, image, data_dup, tex.content.size);
|
||||
|
||||
/* Make sure the image is not marked as "generated". */
|
||||
image->source = IMA_SRC_FILE;
|
||||
image->type = IMA_TYPE_IMAGE;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
struct FbxPbrTextureToSocket {
|
||||
ufbx_material_pbr_map slot;
|
||||
const char *socket;
|
||||
};
|
||||
static const FbxPbrTextureToSocket fbx_pbr_to_socket[] = {
|
||||
{UFBX_MATERIAL_PBR_BASE_COLOR, "Base Color"},
|
||||
{UFBX_MATERIAL_PBR_ROUGHNESS, "Roughness"},
|
||||
{UFBX_MATERIAL_PBR_METALNESS, "Metallic"},
|
||||
{UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, "Diffuse Roughness"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_FACTOR, "Specular IOR Level"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_COLOR, "Specular Tint"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_IOR, "IOR"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, "Anisotropic"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_ROTATION, "Anisotropic Rotation"},
|
||||
{UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, "Transmission Weight"},
|
||||
{UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, "Subsurface Weight"},
|
||||
{UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, "Subsurface Scale"},
|
||||
{UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, "Subsurface Anisotropy"},
|
||||
{UFBX_MATERIAL_PBR_SHEEN_FACTOR, "Sheen Weight"},
|
||||
{UFBX_MATERIAL_PBR_SHEEN_COLOR, "Sheen Tint"},
|
||||
{UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, "Sheen Roughness"},
|
||||
{UFBX_MATERIAL_PBR_COAT_FACTOR, "Coat Weight"},
|
||||
{UFBX_MATERIAL_PBR_COAT_COLOR, "Coat Tint"},
|
||||
{UFBX_MATERIAL_PBR_COAT_ROUGHNESS, "Coat Roughness"},
|
||||
{UFBX_MATERIAL_PBR_COAT_IOR, "Coat IOR"},
|
||||
{UFBX_MATERIAL_PBR_COAT_NORMAL, "Coat Normal"},
|
||||
{UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, "Thin Film Thickness"},
|
||||
{UFBX_MATERIAL_PBR_THIN_FILM_IOR, "Thin Film IOR"},
|
||||
{UFBX_MATERIAL_PBR_EMISSION_FACTOR, "Emission Strength"},
|
||||
{UFBX_MATERIAL_PBR_EMISSION_COLOR, "Emission Color"},
|
||||
{UFBX_MATERIAL_PBR_OPACITY, "Alpha"},
|
||||
{UFBX_MATERIAL_PBR_NORMAL_MAP, "Normal"},
|
||||
{UFBX_MATERIAL_PBR_TANGENT_MAP, "Tangent"},
|
||||
};
|
||||
|
||||
struct FbxStdTextureToSocket {
|
||||
ufbx_material_fbx_map slot;
|
||||
const char *socket;
|
||||
};
|
||||
static const FbxStdTextureToSocket fbx_std_to_socket[] = {
|
||||
{UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, "Alpha"},
|
||||
{UFBX_MATERIAL_FBX_TRANSPARENCY_COLOR, "Alpha"},
|
||||
{UFBX_MATERIAL_FBX_BUMP, "Normal"},
|
||||
};
|
||||
|
||||
static void add_image_texture(Main *bmain,
|
||||
const std::string &file_dir,
|
||||
bNodeTree *ntree,
|
||||
bNode *bsdf,
|
||||
const ufbx_material &fmat,
|
||||
const ufbx_texture *ftex,
|
||||
const char *socket_name,
|
||||
float node_locy,
|
||||
Set<StringRefNull> &done_bsdf_inputs)
|
||||
{
|
||||
Image *image = load_texture_image(bmain, file_dir, *ftex);
|
||||
BLI_assert(image != nullptr);
|
||||
|
||||
/* Set "non-color" color space for all "data" textures. */
|
||||
if (!STR_ELEM(
|
||||
socket_name, "Base Color", "Specular Tint", "Sheen Tint", "Coat Tint", "Emission Color"))
|
||||
{
|
||||
STRNCPY_UTF8(image->colorspace_settings.name,
|
||||
IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_DATA));
|
||||
}
|
||||
|
||||
/* Add texture node and any UV transformations if needed. */
|
||||
bNode *image_node = add_node(ntree, SH_NODE_TEX_IMAGE, node_locx_image, node_locy);
|
||||
BLI_assert(image_node);
|
||||
image_node->id = &image->id;
|
||||
NodeTexImage *tex_image = static_cast<NodeTexImage *>(image_node->storage);
|
||||
|
||||
/* Wrap mode. */
|
||||
tex_image->extension = SHD_IMAGE_EXTENSION_REPEAT;
|
||||
if (ftex->wrap_u == UFBX_WRAP_CLAMP || ftex->wrap_v == UFBX_WRAP_CLAMP) {
|
||||
tex_image->extension = SHD_IMAGE_EXTENSION_EXTEND;
|
||||
}
|
||||
|
||||
/* UV transform. */
|
||||
if (ftex->has_uv_transform) {
|
||||
/* TODO: which UV set to use. */
|
||||
bNode *uvmap = add_node(ntree, SH_NODE_UVMAP, node_locx_texcoord, node_locy);
|
||||
bNode *mapping = add_node(ntree, SH_NODE_MAPPING, node_locx_mapping, node_locy);
|
||||
mapping->custom1 = TEXMAP_TYPE_TEXTURE;
|
||||
set_socket_vector("Location",
|
||||
ftex->uv_transform.translation.x,
|
||||
ftex->uv_transform.translation.y,
|
||||
ftex->uv_transform.translation.z,
|
||||
mapping);
|
||||
ufbx_vec3 rot = ufbx_quat_to_euler(ftex->uv_transform.rotation, UFBX_ROTATION_ORDER_XYZ);
|
||||
set_socket_vector("Rotation", -rot.x, -rot.y, -rot.z, mapping);
|
||||
set_socket_vector("Scale",
|
||||
1.0f / ftex->uv_transform.scale.x,
|
||||
1.0f / ftex->uv_transform.scale.y,
|
||||
1.0f / ftex->uv_transform.scale.z,
|
||||
mapping);
|
||||
|
||||
link_sockets(ntree, uvmap, "UV", mapping, "Vector");
|
||||
link_sockets(ntree, mapping, "Vector", image_node, "Vector");
|
||||
}
|
||||
|
||||
done_bsdf_inputs.add(socket_name);
|
||||
if (STREQ(socket_name, "Normal")) {
|
||||
bNode *normal_node = add_node(ntree, SH_NODE_NORMAL_MAP, node_locx_normalmap, node_locy);
|
||||
link_sockets(ntree, image_node, "Color", normal_node, "Color");
|
||||
link_sockets(ntree, normal_node, "Normal", bsdf, "Normal");
|
||||
|
||||
/* Normal strength: Blender exports it as BumpFactor in FBX built-in properties. */
|
||||
float normal_strength = 1.0f;
|
||||
if (fmat.fbx.bump_factor.has_value) {
|
||||
normal_strength = fmat.fbx.bump_factor.value_real;
|
||||
}
|
||||
set_socket_float("Strength", normal_strength, normal_node);
|
||||
}
|
||||
else {
|
||||
link_sockets(ntree, image_node, "Color", bsdf, socket_name);
|
||||
|
||||
if (STREQ(socket_name, "Base Color") && !done_bsdf_inputs.contains("Alpha")) {
|
||||
/* Link base color alpha (if we have one) to output alpha. */
|
||||
void *lock;
|
||||
ImBuf *ibuf = BKE_image_acquire_ibuf(image, nullptr, &lock);
|
||||
bool has_alpha = ibuf != nullptr && ibuf->can_contain_alpha();
|
||||
BKE_image_release_ibuf(image, ibuf, lock);
|
||||
|
||||
if (has_alpha) {
|
||||
link_sockets(ntree, image_node, "Alpha", bsdf, "Alpha");
|
||||
done_bsdf_inputs.add("Alpha");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void add_image_textures(Main *bmain,
|
||||
const std::string &file_dir,
|
||||
bNodeTree *ntree,
|
||||
bNode *bsdf,
|
||||
const ufbx_material &fmat)
|
||||
{
|
||||
float node_locy = node_locy_top;
|
||||
Set<StringRefNull> done_bsdf_inputs;
|
||||
|
||||
/* We primarily use images from "PBR" FBX mapping. */
|
||||
for (const FbxPbrTextureToSocket &entry : fbx_pbr_to_socket) {
|
||||
BLI_assert(entry.socket != nullptr);
|
||||
if (done_bsdf_inputs.contains(entry.socket)) {
|
||||
continue; /* Already connected. */
|
||||
}
|
||||
|
||||
const ufbx_texture *ftex = fmat.pbr.maps[entry.slot].texture;
|
||||
if (ftex == nullptr || !fmat.pbr.maps[entry.slot].texture_enabled) {
|
||||
/* No texture used for this slot. */
|
||||
continue;
|
||||
}
|
||||
|
||||
add_image_texture(
|
||||
bmain, file_dir, ntree, bsdf, fmat, ftex, entry.socket, node_locy, done_bsdf_inputs);
|
||||
node_locy -= node_locy_step;
|
||||
}
|
||||
|
||||
/* But also support several from the legacy/standard "FBX" material model,
|
||||
* mostly to match behavior of python importer. */
|
||||
for (const FbxStdTextureToSocket &entry : fbx_std_to_socket) {
|
||||
BLI_assert(entry.socket != nullptr);
|
||||
if (done_bsdf_inputs.contains(entry.socket)) {
|
||||
continue; /* Already connected. */
|
||||
}
|
||||
|
||||
const ufbx_texture *ftex = fmat.fbx.maps[entry.slot].texture;
|
||||
if (ftex == nullptr || !fmat.fbx.maps[entry.slot].texture_enabled) {
|
||||
/* No texture used for this slot. */
|
||||
continue;
|
||||
}
|
||||
|
||||
add_image_texture(
|
||||
bmain, file_dir, ntree, bsdf, fmat, ftex, entry.socket, node_locy, done_bsdf_inputs);
|
||||
node_locy -= node_locy_step;
|
||||
}
|
||||
}
|
||||
|
||||
Material *import_material(Main *bmain, const std::string &base_dir, const ufbx_material &fmat)
|
||||
{
|
||||
Material *mat = BKE_material_add(bmain, fmat.name.data);
|
||||
id_us_min(&mat->id);
|
||||
|
||||
bNodeTree *ntree = mat->nodetree;
|
||||
bNode *bsdf = add_node(ntree, SH_NODE_BSDF_PRINCIPLED, node_locx_bsdf, node_locy_top);
|
||||
bNode *output = add_node(ntree, SH_NODE_OUTPUT_MATERIAL, node_locx_output, node_locy_top);
|
||||
set_bsdf_socket_values(bsdf, mat, fmat);
|
||||
add_image_textures(bmain, base_dir, ntree, bsdf, fmat);
|
||||
link_sockets(ntree, bsdf, "BSDF", output, "Surface");
|
||||
bke::node_set_active(*ntree, *output);
|
||||
|
||||
mat->nodetree = ntree;
|
||||
|
||||
BKE_ntree_update_after_single_tree_change(*bmain, *mat->nodetree);
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,22 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
struct ufbx_material;
|
||||
namespace blender {
|
||||
|
||||
struct Main;
|
||||
struct Material;
|
||||
namespace io::fbx {
|
||||
|
||||
Material *import_material(Main *bmain, const std::string &base_dir, const ufbx_material &fmat);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
663
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_mesh.cc
Normal file
663
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_mesh.cc
Normal file
@@ -0,0 +1,663 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_attribute.hh"
|
||||
#include "BKE_deform.hh"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_material.hh"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_modifier.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_object_deform.h"
|
||||
|
||||
#include "BLI_color_types.hh"
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_math_color.h"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
#include "BLI_task.hh"
|
||||
#include "BLI_vector_set.hh"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
#include "DNA_key_types.h"
|
||||
#include "DNA_meshdata_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
#include "IO_validate.hh"
|
||||
|
||||
#include "fbx_import_mesh.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
static CLG_LogRef LOG = {"io.fbx"};
|
||||
|
||||
static constexpr const char *temp_custom_normals_name = "fbx_temp_custom_normals";
|
||||
|
||||
static bool is_skin_deformer_usable(const ufbx_mesh *mesh, const ufbx_skin_deformer *skin)
|
||||
{
|
||||
return mesh != nullptr && skin != nullptr && skin->clusters.count > 0 &&
|
||||
mesh->num_vertices > 0 && skin->vertices.count == mesh->num_vertices;
|
||||
}
|
||||
|
||||
static void import_vertex_positions(const ufbx_mesh *fmesh, Mesh *mesh)
|
||||
{
|
||||
MutableSpan<float3> positions = mesh->vert_positions_for_write();
|
||||
#if 0 // @TODO: "bake" skinned meshes
|
||||
if (skin != nullptr) {
|
||||
/* For a skinned mesh, transform the vertices into bind pose position, in local space. */
|
||||
const ufbx_matrix &geom_to_world = fmesh->instances[0]->geometry_to_world;
|
||||
ufbx_matrix world_to_geom = ufbx_matrix_invert(&geom_to_world);
|
||||
for (size_t i = 0; i < fmesh->vertex_position.values.count; i++) {
|
||||
ufbx_matrix skin_mat = ufbx_get_skin_vertex_matrix(skin, i, &geom_to_world);
|
||||
skin_mat = ufbx_matrix_mul(&world_to_geom, &skin_mat);
|
||||
ufbx_vec3 val = ufbx_transform_position(&skin_mat, fmesh->vertex_position.values[i]);
|
||||
positions[i] = float3(val.x, val.y, val.z);
|
||||
//@TODO: skin normals
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
BLI_assert(positions.size() == fmesh->vertex_position.values.count);
|
||||
for (const int64_t i : positions.index_range()) {
|
||||
ufbx_vec3 val = fmesh->vertex_position.values[i];
|
||||
positions[i] = float3(val.x, val.y, val.z);
|
||||
}
|
||||
}
|
||||
|
||||
static void import_faces(const ufbx_mesh *fmesh, Mesh *mesh)
|
||||
{
|
||||
MutableSpan<int> face_offsets = mesh->face_offsets_for_write();
|
||||
MutableSpan<int> corner_verts = mesh->corner_verts_for_write();
|
||||
BLI_assert((face_offsets.size() == fmesh->num_faces + 1) ||
|
||||
(face_offsets.is_empty() && fmesh->num_faces == 0));
|
||||
for (size_t face_idx = 0; face_idx < fmesh->num_faces; face_idx++) {
|
||||
//@TODO: skip < 3 vertex faces?
|
||||
const ufbx_face &fface = fmesh->faces[face_idx];
|
||||
face_offsets[face_idx] = fface.index_begin;
|
||||
for (uint32_t i = 0; i < fface.num_indices; i++) {
|
||||
const uint32_t corner_idx = fface.index_begin + i;
|
||||
corner_verts[corner_idx] = fmesh->vertex_indices[corner_idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void import_face_material_indices(const ufbx_mesh *fmesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
if (fmesh->face_material.count == fmesh->num_faces) {
|
||||
bke::SpanAttributeWriter<int> materials = attributes.lookup_or_add_for_write_only_span<int>(
|
||||
"material_index", bke::AttrDomain::Face);
|
||||
for (const int64_t i : materials.span.index_range()) {
|
||||
materials.span[i] = fmesh->face_material[i];
|
||||
}
|
||||
materials.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_face_smoothing(const ufbx_mesh *fmesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
if (fmesh->face_smoothing.count > 0 && fmesh->face_smoothing.count == fmesh->num_faces) {
|
||||
bke::SpanAttributeWriter<bool> smooth = attributes.lookup_or_add_for_write_only_span<bool>(
|
||||
"sharp_face", bke::AttrDomain::Face);
|
||||
for (const int64_t i : smooth.span.index_range()) {
|
||||
smooth.span[i] = !fmesh->face_smoothing[i];
|
||||
}
|
||||
smooth.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_edges(const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
MutableSpan<int2> edges = mesh->edges_for_write();
|
||||
BLI_assert(edges.size() == fmesh->num_edges);
|
||||
for (size_t i = 0; i < fmesh->num_edges; i++) {
|
||||
const ufbx_edge &fedge = fmesh->edges[i];
|
||||
const int va = fmesh->vertex_indices[fedge.a];
|
||||
const int vb = fmesh->vertex_indices[fedge.b];
|
||||
edges[i] = int2(va, vb);
|
||||
}
|
||||
|
||||
/* Edge attributes are written here in the same order as the FBX edges. Mesh validation
|
||||
* preserves edge attributes when removing degenerate edges or computing missing ones. */
|
||||
if (fmesh->edge_crease.count > 0 && fmesh->edge_crease.count == fmesh->num_edges) {
|
||||
bke::SpanAttributeWriter<float> creases = attributes.lookup_or_add_for_write_only_span<float>(
|
||||
"crease_edge", bke::AttrDomain::Edge);
|
||||
for (int64_t i = 0; i < fmesh->num_edges; i++) {
|
||||
/* Python fbx importer was squaring the incoming crease values. */
|
||||
creases.span[i] = sqrtf(fmesh->edge_crease[i]);
|
||||
}
|
||||
creases.finish();
|
||||
}
|
||||
|
||||
if (fmesh->edge_smoothing.count > 0 && fmesh->edge_smoothing.count == fmesh->num_edges) {
|
||||
bke::SpanAttributeWriter<bool> sharp = attributes.lookup_or_add_for_write_only_span<bool>(
|
||||
"sharp_edge", bke::AttrDomain::Edge);
|
||||
for (int64_t i = 0; i < fmesh->num_edges; i++) {
|
||||
sharp.span[i] = !fmesh->edge_smoothing[i];
|
||||
}
|
||||
sharp.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_uvs(const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
bke::MutableAttributeAccessor &attributes,
|
||||
AttributeOwner attr_owner)
|
||||
{
|
||||
bool set_active_uv = true;
|
||||
for (const ufbx_uv_set &fuv_set : fmesh->uv_sets) {
|
||||
std::string attr_name = BKE_attribute_calc_unique_name(attr_owner, fuv_set.name.data);
|
||||
if (set_active_uv) {
|
||||
mesh->uv_maps_active_set(attr_name);
|
||||
mesh->uv_maps_default_set(attr_name);
|
||||
set_active_uv = false;
|
||||
}
|
||||
bke::SpanAttributeWriter<float2> uvs = attributes.lookup_or_add_for_write_only_span<float2>(
|
||||
attr_name, bke::AttrDomain::Corner);
|
||||
BLI_assert(fuv_set.vertex_uv.indices.count == uvs.span.size());
|
||||
for (const int64_t i : uvs.span.index_range()) {
|
||||
const int val_idx = fuv_set.vertex_uv.indices[i];
|
||||
const ufbx_vec2 &uv = fuv_set.vertex_uv.values[val_idx];
|
||||
uvs.span[i] = float2(uv.x, uv.y);
|
||||
}
|
||||
uvs.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_colors(const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
bke::MutableAttributeAccessor &attributes,
|
||||
AttributeOwner attr_owner,
|
||||
eFBXVertexColorMode color_mode)
|
||||
{
|
||||
std::string first_color_name;
|
||||
for (const ufbx_color_set &fcol_set : fmesh->color_sets) {
|
||||
std::string attr_name = BKE_attribute_calc_unique_name(attr_owner, fcol_set.name.data);
|
||||
if (first_color_name.empty()) {
|
||||
first_color_name = attr_name;
|
||||
}
|
||||
if (color_mode == eFBXVertexColorMode::sRGB) {
|
||||
/* sRGB colors, use 4 bytes per color. */
|
||||
bke::SpanAttributeWriter<ColorGeometry4b> cols =
|
||||
attributes.lookup_or_add_for_write_only_span<ColorGeometry4b>(attr_name,
|
||||
bke::AttrDomain::Corner);
|
||||
BLI_assert(fcol_set.vertex_color.indices.count == cols.span.size());
|
||||
for (const int64_t i : cols.span.index_range()) {
|
||||
const int val_idx = fcol_set.vertex_color.indices[i];
|
||||
const ufbx_vec4 &col = fcol_set.vertex_color.values[val_idx];
|
||||
/* Note: color values are expected to already be in sRGB space. */
|
||||
float4 fcol = float4(col.x, col.y, col.z, col.w);
|
||||
uchar4 bcol;
|
||||
rgba_float_to_uchar(bcol, fcol);
|
||||
cols.span[i] = ColorGeometry4b(bcol);
|
||||
}
|
||||
cols.finish();
|
||||
}
|
||||
else if (color_mode == eFBXVertexColorMode::Linear) {
|
||||
/* Linear colors, use 4 floats per color. */
|
||||
bke::SpanAttributeWriter<ColorGeometry4f> cols =
|
||||
attributes.lookup_or_add_for_write_only_span<ColorGeometry4f>(attr_name,
|
||||
bke::AttrDomain::Corner);
|
||||
BLI_assert(fcol_set.vertex_color.indices.count == cols.span.size());
|
||||
for (const int64_t i : cols.span.index_range()) {
|
||||
const int val_idx = fcol_set.vertex_color.indices[i];
|
||||
const ufbx_vec4 &col = fcol_set.vertex_color.values[val_idx];
|
||||
cols.span[i] = ColorGeometry4f(col.x, col.y, col.z, col.w);
|
||||
}
|
||||
cols.finish();
|
||||
}
|
||||
else {
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
}
|
||||
if (!first_color_name.empty()) {
|
||||
mesh->active_color_attribute = BLI_strdup(first_color_name.c_str());
|
||||
mesh->default_color_attribute = BLI_strdup(first_color_name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static bool import_normals_into_temp_attribute(const ufbx_mesh *fmesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
if (!fmesh->vertex_normal.exists) {
|
||||
return false;
|
||||
}
|
||||
bke::SpanAttributeWriter<float3> normals = attributes.lookup_or_add_for_write_only_span<float3>(
|
||||
temp_custom_normals_name, bke::AttrDomain::Corner);
|
||||
BLI_assert(fmesh->vertex_normal.indices.count == normals.span.size());
|
||||
for (const int64_t i : normals.span.index_range()) {
|
||||
const int val_idx = fmesh->vertex_normal.indices[i];
|
||||
const ufbx_vec3 &normal = fmesh->vertex_normal.values[val_idx];
|
||||
normals.span[i] = float3(normal.x, normal.y, normal.z);
|
||||
}
|
||||
normals.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
static VectorSet<std::string> get_skin_bone_name_set(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh)
|
||||
{
|
||||
VectorSet<std::string> name_set;
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string bone_name = mapping.node_to_name.lookup_default(cluster->bone_node, "");
|
||||
name_set.add(bone_name);
|
||||
}
|
||||
}
|
||||
return name_set;
|
||||
}
|
||||
|
||||
static void import_skin_vertex_groups(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh,
|
||||
Mesh *mesh)
|
||||
{
|
||||
if (fmesh->skin_deformers.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* A single mesh can be skinned by several armatures, so we need to build bone (vertex group)
|
||||
* name set, taking all skin deformers into account. */
|
||||
VectorSet<std::string> bone_set = get_skin_bone_name_set(mapping, fmesh);
|
||||
if (bone_set.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
MutableSpan<MDeformVert> dverts = mesh->deform_verts_for_write();
|
||||
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
std::string bone_name = mapping.node_to_name.lookup_default(cluster->bone_node, "");
|
||||
const int group_index = bone_set.index_of_try(bone_name);
|
||||
if (group_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int64_t i = 0; i < cluster->num_weights; i++) {
|
||||
const int vertex = cluster->vertices[i];
|
||||
if (validate::index_in_range(vertex, dverts.size())) {
|
||||
MDeformWeight *dw = BKE_defvert_ensure_index(&dverts[vertex], group_index);
|
||||
dw->weight = cluster->weights[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool import_blend_shapes(Main &bmain,
|
||||
FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh,
|
||||
Mesh *mesh)
|
||||
{
|
||||
Key *mesh_key = nullptr;
|
||||
for (const ufbx_blend_deformer *fdeformer : fmesh->blend_deformers) {
|
||||
for (const ufbx_blend_channel *fchan : fdeformer->channels) {
|
||||
/* In theory fbx supports multiple keyframes within one blend shape
|
||||
* channel; we only take the final target keyframe. */
|
||||
if (fchan->target_shape == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mesh_key == nullptr) {
|
||||
mesh_key = BKE_key_add(&bmain, &mesh->id);
|
||||
mesh_key->type = KEY_RELATIVE;
|
||||
mesh->key = mesh_key;
|
||||
|
||||
KeyBlock *kb = BKE_keyblock_add(mesh_key, nullptr);
|
||||
BKE_keyblock_convert_from_mesh(mesh, mesh_key, kb);
|
||||
}
|
||||
|
||||
KeyBlock *kb = BKE_keyblock_add(mesh_key, fchan->target_shape->name.data);
|
||||
kb->curval = fchan->weight;
|
||||
BKE_keyblock_convert_from_mesh(mesh, mesh_key, kb);
|
||||
if (!kb->data) {
|
||||
/* Nothing to do. This can happen if the mesh has no vertices. */
|
||||
continue;
|
||||
}
|
||||
float3 *kb_data = static_cast<float3 *>(kb->data);
|
||||
for (size_t i = 0; i < fchan->target_shape->num_offsets; i++) {
|
||||
const int idx = fchan->target_shape->offset_vertices[i];
|
||||
if (!validate::index_in_range(idx, mesh->verts_num)) {
|
||||
continue;
|
||||
}
|
||||
const ufbx_vec3 &delta = fchan->target_shape->position_offsets[i];
|
||||
kb_data[idx] += float3(delta.x, delta.y, delta.z);
|
||||
}
|
||||
mapping.el_to_shape_key.add(&fchan->element, mesh_key);
|
||||
}
|
||||
}
|
||||
return mesh_key != nullptr;
|
||||
}
|
||||
|
||||
/* Handle Blender-specific "FullWeights" that for each blend shape also create
|
||||
* a weighted vertex group for itself. */
|
||||
static void import_blend_shape_full_weights(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
Object *obj)
|
||||
{
|
||||
for (const ufbx_blend_deformer *fdeformer : fmesh->blend_deformers) {
|
||||
for (const ufbx_blend_channel *fchan : fdeformer->channels) {
|
||||
Key *key = mapping.el_to_shape_key.lookup_default(&fchan->element, nullptr);
|
||||
if (fchan->target_shape == nullptr || key == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (fchan->target_shape->offset_weights.count != fchan->target_shape->num_offsets) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBlock *kb = BKE_keyblock_find_name(key, fchan->target_shape->name.data);
|
||||
if (kb == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Ignore cases where all weights are 1.0 (group has no effect),
|
||||
* and cases where any weights are outside of 0..1 range (apparently some files have
|
||||
* invalid negative weights and should be ignored). */
|
||||
bool all_one = true;
|
||||
bool all_unorm = true;
|
||||
for (ufbx_real w : fchan->target_shape->offset_weights) {
|
||||
if (w != 1.0) {
|
||||
all_one = false;
|
||||
}
|
||||
if (w < 0.0 || w > 1.0) {
|
||||
all_unorm = false;
|
||||
}
|
||||
}
|
||||
if (all_one || !all_unorm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int group_index = BKE_defgroup_name_index(&mesh->vertex_group_names, kb->name);
|
||||
if (group_index < 0) {
|
||||
BKE_object_defgroup_add_name(obj, kb->name);
|
||||
group_index = BKE_defgroup_name_index(&mesh->vertex_group_names, kb->name);
|
||||
if (group_index < 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
MutableSpan<MDeformVert> dverts = mesh->deform_verts_for_write();
|
||||
for (size_t i = 0; i < fchan->target_shape->num_offsets; i++) {
|
||||
const int idx = fchan->target_shape->offset_vertices[i];
|
||||
if (validate::index_in_range(idx, dverts.size())) {
|
||||
const float w = fchan->target_shape->offset_weights[i];
|
||||
MDeformWeight *dw = BKE_defvert_ensure_index(&dverts[idx], group_index);
|
||||
dw->weight = w;
|
||||
}
|
||||
}
|
||||
|
||||
STRNCPY_UTF8(kb->vgroup, kb->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void import_meshes(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms)
|
||||
{
|
||||
/* Create Mesh objects outside of Main, in parallel. */
|
||||
Vector<Mesh *> meshes(fbx.meshes.count);
|
||||
threading::parallel_for_each(IndexRange(fbx.meshes.count), [&](const int64_t index) {
|
||||
const ufbx_mesh *fmesh = fbx.meshes.data[index];
|
||||
if (fmesh->instances.count == 0) {
|
||||
meshes[index] = nullptr; /* Ignore if not used by any objects. */
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validate::size_fits_in_int(fmesh->num_vertices) ||
|
||||
!validate::size_fits_in_int(fmesh->num_edges) ||
|
||||
!validate::size_fits_in_int(fmesh->num_faces) ||
|
||||
!validate::size_fits_in_int(fmesh->num_indices))
|
||||
{
|
||||
CLOG_WARN(&LOG, "Mesh '%s' too large to import, exceeds max int size", fmesh->name.data);
|
||||
meshes[index] = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Create Mesh outside of main. */
|
||||
Mesh *mesh = BKE_mesh_new_nomain(
|
||||
fmesh->num_vertices, fmesh->num_edges, fmesh->num_faces, fmesh->num_indices);
|
||||
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
|
||||
AttributeOwner attr_owner = AttributeOwner::from_id(&mesh->id);
|
||||
|
||||
import_vertex_positions(fmesh, mesh);
|
||||
import_faces(fmesh, mesh);
|
||||
import_face_material_indices(fmesh, attributes);
|
||||
import_face_smoothing(fmesh, attributes);
|
||||
import_edges(fmesh, mesh, attributes);
|
||||
import_uvs(fmesh, mesh, attributes, attr_owner);
|
||||
if (params.vertex_colors != eFBXVertexColorMode::None) {
|
||||
import_colors(fmesh, mesh, attributes, attr_owner, params.vertex_colors);
|
||||
}
|
||||
bool has_custom_normals = false;
|
||||
if (params.use_custom_normals) {
|
||||
/* Mesh validation below can alter the mesh, so we first write custom normals
|
||||
* into a temporary custom corner domain attribute, and then re-apply that
|
||||
* data as custom normals after the validation. */
|
||||
has_custom_normals = import_normals_into_temp_attribute(fmesh, attributes);
|
||||
}
|
||||
import_skin_vertex_groups(mapping, fmesh, mesh);
|
||||
|
||||
/* Add vertex groups to the object. */
|
||||
VectorSet<std::string> bone_set = get_skin_bone_name_set(mapping, fmesh);
|
||||
for (const std::string &name : bone_set) {
|
||||
bDeformGroup *defgroup = MEM_new<bDeformGroup>("bDeformGroup");
|
||||
StringRef(name).copy_utf8_truncated(defgroup->name);
|
||||
BLI_addtail(&mesh->vertex_group_names, defgroup);
|
||||
}
|
||||
|
||||
/* FBX files may not contain all edges, so missing edges must be added here.
|
||||
* Validation will do this, and otherwise calculate them explicitly. */
|
||||
if (params.validate_meshes) {
|
||||
const bool allow_missing_edges = true;
|
||||
#ifndef NDEBUG
|
||||
const bool verbose_validate = true;
|
||||
#else
|
||||
const bool verbose_validate = false;
|
||||
#endif
|
||||
bke::mesh_validate(*mesh, verbose_validate, allow_missing_edges);
|
||||
}
|
||||
else {
|
||||
bke::mesh_calc_edges(*mesh, true, false);
|
||||
}
|
||||
|
||||
if (has_custom_normals) {
|
||||
/* Actually set custom normals after the validation. */
|
||||
bke::SpanAttributeWriter<float3> normals =
|
||||
attributes.lookup_or_add_for_write_only_span<float3>(temp_custom_normals_name,
|
||||
bke::AttrDomain::Corner);
|
||||
bke::mesh_set_custom_normals(*mesh, normals.span);
|
||||
normals.finish();
|
||||
attributes.remove(temp_custom_normals_name);
|
||||
}
|
||||
|
||||
meshes[index] = mesh;
|
||||
});
|
||||
|
||||
/* Create final mesh objects in Main, serially. And do steps that need to be done on the final
|
||||
* objects. */
|
||||
for (int64_t index : meshes.index_range()) {
|
||||
Mesh *mesh = meshes[index];
|
||||
if (mesh == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const ufbx_mesh *fmesh = fbx.meshes[index];
|
||||
BLI_assert(fmesh != nullptr);
|
||||
|
||||
Mesh *mesh_main = static_cast<Mesh *>(
|
||||
BKE_object_obdata_add_from_type(&bmain, OB_MESH, get_fbx_name(fmesh->name, "Mesh")));
|
||||
BKE_mesh_nomain_to_mesh(mesh, mesh_main, nullptr);
|
||||
meshes[index] = mesh_main;
|
||||
mesh = mesh_main;
|
||||
if (params.use_custom_props) {
|
||||
read_custom_properties(fmesh->props, mesh->id, params.props_enum_as_string);
|
||||
}
|
||||
|
||||
const bool any_shapes = import_blend_shapes(bmain, mapping, fmesh, mesh);
|
||||
|
||||
/* Create objects that use this mesh. */
|
||||
for (const ufbx_node *node : fmesh->instances) {
|
||||
std::string name;
|
||||
if (node->is_geometry_transform_helper) {
|
||||
/* Name geometry transform adjustment helpers with parent name and _GeomAdjust suffix. */
|
||||
name = get_fbx_name(node->parent->name) + std::string("_GeomAdjust");
|
||||
}
|
||||
else {
|
||||
name = get_fbx_name(node->name);
|
||||
}
|
||||
Object *obj = BKE_object_add_only_object(&bmain, OB_MESH, name.c_str());
|
||||
obj->data = id_cast<ID *>(mesh_main);
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
|
||||
if (any_shapes) {
|
||||
obj->shapenr = 1;
|
||||
}
|
||||
|
||||
bool matrix_already_set = false;
|
||||
|
||||
/* Skinned mesh. */
|
||||
if (fmesh->skin_deformers.count > 0) {
|
||||
/* Add armature modifiers for each skin deformer. */
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
Object *arm_obj = nullptr;
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
arm_obj = mapping.bone_to_armature.lookup_default(cluster->bone_node, nullptr);
|
||||
if (arm_obj != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Add armature modifier. */
|
||||
if (arm_obj != nullptr) {
|
||||
ModifierData *md = BKE_modifier_new(eModifierType_Armature);
|
||||
STRNCPY_UTF8(md->name, BKE_id_name(arm_obj->id));
|
||||
BLI_addtail(&obj->modifiers, md);
|
||||
BKE_modifiers_persistent_uid_init(*obj, *md);
|
||||
ArmatureModifierData *ad = reinterpret_cast<ArmatureModifierData *>(md);
|
||||
ad->object = arm_obj;
|
||||
|
||||
if (!matrix_already_set) {
|
||||
matrix_already_set = true;
|
||||
obj->parent = arm_obj;
|
||||
|
||||
/* We are setting mesh parent to the armature, so set the matrix that is
|
||||
* armature-local. Note that the matrix needs to be relative to the FBX
|
||||
* node matrix (not the root bone pose matrix). */
|
||||
ufbx_matrix world_to_arm = mapping.armature_world_to_arm_node_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
ufbx_matrix world_to_arm_pose = mapping.armature_world_to_arm_pose_matrix
|
||||
.lookup_default(arm_obj, ufbx_identity_matrix);
|
||||
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(&world_to_arm, &node->geometry_to_world);
|
||||
ufbx_matrix_to_obj(mtx, obj);
|
||||
|
||||
/* Setup parent inverse matrix of the mesh, to account for the mesh possibly being in
|
||||
* different bind pose than what the node is at. */
|
||||
ufbx_matrix mtx_inv = ufbx_matrix_invert(&mtx);
|
||||
ufbx_matrix mtx_world = mapping.get_node_bind_matrix(node);
|
||||
ufbx_matrix mtx_parent_inverse = ufbx_matrix_mul(&mtx_world, &mtx_inv);
|
||||
mtx_parent_inverse = ufbx_matrix_mul(&world_to_arm_pose, &mtx_parent_inverse);
|
||||
matrix_to_m44(mtx_parent_inverse, obj->parentinv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (any_shapes) {
|
||||
import_blend_shape_full_weights(mapping, fmesh, mesh, obj);
|
||||
}
|
||||
|
||||
/* Assign materials. */
|
||||
if (fmesh->materials.count > 0 && node->materials.count == fmesh->materials.count) {
|
||||
int mat_index = 0;
|
||||
for (size_t mi = 0; mi < fmesh->materials.count; mi++) {
|
||||
const ufbx_material *mesh_fmat = fmesh->materials[mi];
|
||||
const ufbx_material *node_fmat = node->materials[mi];
|
||||
Material *mesh_mat = mapping.mat_to_material.lookup_default(mesh_fmat, nullptr);
|
||||
Material *node_mat = mapping.mat_to_material.lookup_default(node_fmat, nullptr);
|
||||
if (mesh_mat != nullptr) {
|
||||
mat_index++;
|
||||
/* Assign material to the data block. */
|
||||
BKE_object_material_assign_single_obdata(&bmain, obj, mesh_mat, mat_index);
|
||||
|
||||
/* If object material is different, assign that to object. */
|
||||
if (!ELEM(node_mat, nullptr, mesh_mat)) {
|
||||
BKE_object_material_assign(&bmain, obj, node_mat, mat_index, BKE_MAT_ASSIGN_OBJECT);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mat_index > 0) {
|
||||
obj->actcol = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Subdivision. */
|
||||
if (params.import_subdivision &&
|
||||
fmesh->subdivision_display_mode != UFBX_SUBDIVISION_DISPLAY_DISABLED &&
|
||||
(fmesh->subdivision_preview_levels > 0 || fmesh->subdivision_render_levels > 0))
|
||||
{
|
||||
ModifierData *md = BKE_modifier_new(eModifierType_Subsurf);
|
||||
BLI_addtail(&obj->modifiers, md);
|
||||
BKE_modifiers_persistent_uid_init(*obj, *md);
|
||||
|
||||
SubsurfModifierData *ssd = reinterpret_cast<SubsurfModifierData *>(md);
|
||||
ssd->subdivType = SUBSURF_TYPE_CATMULL_CLARK;
|
||||
ssd->levels = fmesh->subdivision_preview_levels;
|
||||
ssd->renderLevels = fmesh->subdivision_render_levels;
|
||||
ssd->boundary_smooth = fmesh->subdivision_boundary ==
|
||||
UFBX_SUBDIVISION_BOUNDARY_SHARP_CORNERS ?
|
||||
SUBSURF_BOUNDARY_SMOOTH_PRESERVE_CORNERS :
|
||||
SUBSURF_BOUNDARY_SMOOTH_ALL;
|
||||
}
|
||||
|
||||
if (params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, params.props_enum_as_string);
|
||||
}
|
||||
if (!matrix_already_set) {
|
||||
node_matrix_to_obj(node, obj, mapping);
|
||||
}
|
||||
mapping.el_to_object.add(&node->element, obj);
|
||||
mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct FBXImportParams;
|
||||
struct Main;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void import_meshes(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
309
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.cc
Normal file
309
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.cc
Normal file
@@ -0,0 +1,309 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_idprop.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_object_types.hh"
|
||||
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
const char *get_fbx_name(const ufbx_string &name, const char *def)
|
||||
{
|
||||
return name.length > 0 ? name.data : def;
|
||||
}
|
||||
|
||||
void matrix_to_m44(const ufbx_matrix &src, float dst[4][4])
|
||||
{
|
||||
dst[0][0] = src.m00;
|
||||
dst[1][0] = src.m01;
|
||||
dst[2][0] = src.m02;
|
||||
dst[3][0] = src.m03;
|
||||
dst[0][1] = src.m10;
|
||||
dst[1][1] = src.m11;
|
||||
dst[2][1] = src.m12;
|
||||
dst[3][1] = src.m13;
|
||||
dst[0][2] = src.m20;
|
||||
dst[1][2] = src.m21;
|
||||
dst[2][2] = src.m22;
|
||||
dst[3][2] = src.m23;
|
||||
dst[0][3] = 0.0f;
|
||||
dst[1][3] = 0.0f;
|
||||
dst[2][3] = 0.0f;
|
||||
dst[3][3] = 1.0f;
|
||||
}
|
||||
|
||||
ufbx_matrix calc_bone_pose_matrix(const ufbx_transform &local_xform,
|
||||
const ufbx_node &node,
|
||||
const ufbx_matrix &local_bind_inv_matrix)
|
||||
{
|
||||
ufbx_transform xform = local_xform;
|
||||
|
||||
/* For bones that have "ignore parent scale" on them, ufbx helpfully applies global scale to
|
||||
* the evaluated transform. However we really need to get local transform without global
|
||||
* scale, so undo that. */
|
||||
if (node.adjust_post_scale != 1.0) {
|
||||
xform.scale.x /= node.adjust_post_scale;
|
||||
xform.scale.y /= node.adjust_post_scale;
|
||||
xform.scale.z /= node.adjust_post_scale;
|
||||
}
|
||||
|
||||
/* Transformed to the bind transform in joint-local space. */
|
||||
ufbx_matrix matrix = ufbx_transform_to_matrix(&xform);
|
||||
matrix = ufbx_matrix_mul(&local_bind_inv_matrix, &matrix);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
void ufbx_matrix_to_obj(const ufbx_matrix &mtx, Object *obj)
|
||||
{
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "init NODE %s self.matrix:\n", obj->id.name + 2);
|
||||
print_matrix(mtx);
|
||||
#endif
|
||||
|
||||
float obmat[4][4];
|
||||
matrix_to_m44(mtx, obmat);
|
||||
BKE_object_apply_mat4(obj, obmat, true, false);
|
||||
BKE_object_to_mat4(obj, obj->runtime->object_to_world.ptr());
|
||||
}
|
||||
|
||||
void node_matrix_to_obj(const ufbx_node *node, Object *obj, const FbxElementMapping &mapping)
|
||||
{
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(node->node_depth < 2 ? &node->node_to_world :
|
||||
&node->node_to_parent,
|
||||
&node->geometry_to_node);
|
||||
|
||||
/* Handle case of an object parented to a bone: need to set
|
||||
* bone as parent, and make transform be at the end of the bone. */
|
||||
const ufbx_node *parbone = node->parent;
|
||||
if (obj->parent == nullptr && parbone && mapping.node_is_blender_bone.contains(parbone)) {
|
||||
Object *arm = mapping.bone_to_armature.lookup_default(parbone, nullptr);
|
||||
if (arm != nullptr) {
|
||||
ufbx_matrix offset_mtx = ufbx_identity_matrix;
|
||||
offset_mtx.cols[3].y = -mapping.bone_to_length.lookup_default(parbone, 0.0);
|
||||
if (mapping.node_is_blender_bone.contains(node)) {
|
||||
/* The node itself is a "fake bone", in which case parent it to the matching
|
||||
* fake bone, and matrix is just what puts transform at the bone tail. */
|
||||
parbone = node;
|
||||
mtx = offset_mtx;
|
||||
}
|
||||
else {
|
||||
mtx = ufbx_matrix_mul(&offset_mtx, &mtx);
|
||||
}
|
||||
|
||||
obj->parent = arm;
|
||||
obj->partype = PARBONE;
|
||||
STRNCPY_UTF8(obj->parsubstr, mapping.node_to_name.lookup_default(parbone, "").c_str());
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file,
|
||||
"parent CHILD %s to ARM %s BONE %s bone_child_mtx:\n",
|
||||
node->name.data,
|
||||
arm->id.name + 2,
|
||||
parbone->name.data);
|
||||
print_matrix(offset_mtx);
|
||||
fprintf(g_debug_file, "- child matrix:\n");
|
||||
print_matrix(mtx);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
ufbx_matrix_to_obj(mtx, obj);
|
||||
}
|
||||
|
||||
static void read_ufbx_property(const ufbx_prop &prop, IDProperty *idgroup, bool enums_as_strings)
|
||||
{
|
||||
IDProperty *idprop = nullptr;
|
||||
IDPropertyTemplate val = {0};
|
||||
//@TODO: validate_blend_names on the property name
|
||||
const char *name = prop.name.data;
|
||||
|
||||
switch (prop.type) {
|
||||
case UFBX_PROP_BOOLEAN:
|
||||
val.i = prop.value_int;
|
||||
idprop = IDP_New(IDP_BOOLEAN, &val, name);
|
||||
break;
|
||||
case UFBX_PROP_INTEGER: {
|
||||
bool parsed_as_enum = false;
|
||||
if (enums_as_strings && (prop.flags & UFBX_PROP_FLAG_VALUE_STR) &&
|
||||
(prop.value_str.length > 0))
|
||||
{
|
||||
/* "Enum" property with integer value, and enum names as `~` separated string. */
|
||||
const char *tilde = prop.value_str.data;
|
||||
int enum_index = -1;
|
||||
while (true) {
|
||||
const char *tilde_start = tilde;
|
||||
tilde = BLI_strchr_or_end(tilde_start, '~');
|
||||
if (tilde == tilde_start) {
|
||||
break;
|
||||
}
|
||||
/* We have an enum value string. */
|
||||
enum_index++;
|
||||
if (enum_index == prop.value_int) {
|
||||
/* Found the needed one. */
|
||||
parsed_as_enum = true;
|
||||
std::string str_val = StringRef(tilde_start, tilde).trim();
|
||||
val.string.str = str_val.c_str();
|
||||
val.string.len = str_val.size() + 1; /* .len needs to include null terminator. */
|
||||
val.string.subtype = IDP_STRING_SUB_UTF8;
|
||||
idprop = IDP_New(IDP_STRING, &val, name);
|
||||
break;
|
||||
}
|
||||
if (tilde[0] == 0) {
|
||||
break;
|
||||
}
|
||||
tilde++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed_as_enum) {
|
||||
val.i = prop.value_int;
|
||||
idprop = IDP_New(IDP_INT, &val, name);
|
||||
}
|
||||
|
||||
} break;
|
||||
case UFBX_PROP_NUMBER:
|
||||
val.d = prop.value_real;
|
||||
idprop = IDP_New(IDP_DOUBLE, &val, name);
|
||||
break;
|
||||
case UFBX_PROP_STRING:
|
||||
if (STREQ(name, "UDP3DSMAX")) {
|
||||
/* 3dsmax user properties are coming as `UDP3DSMAX` property. Parse them
|
||||
* as multi-line text, splitting across `=` within each line. */
|
||||
const char *line = prop.value_str.data;
|
||||
while (true) {
|
||||
const char *line_start = line;
|
||||
line = BLI_strchr_or_end(line_start, '\n');
|
||||
if (line == line_start) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* We have a line, split it by '=' and trim name/value. */
|
||||
const char *eq_pos = line_start;
|
||||
while (eq_pos != line && eq_pos[0] != '=') {
|
||||
eq_pos++;
|
||||
}
|
||||
if (eq_pos[0] == '=') {
|
||||
std::string str_name = StringRef(line_start, eq_pos).trim();
|
||||
std::string str_val = StringRef(eq_pos + 1, line).trim();
|
||||
//@TODO validate_blend_names on str_name
|
||||
val.string.str = str_val.c_str();
|
||||
val.string.len = str_val.size() + 1; /* .len needs to include null terminator. */
|
||||
val.string.subtype = IDP_STRING_SUB_UTF8;
|
||||
IDProperty *str_prop = IDP_New(IDP_STRING, &val, str_name.c_str());
|
||||
IDP_AddToGroup(idgroup, str_prop);
|
||||
}
|
||||
|
||||
if (line[0] == 0) {
|
||||
break;
|
||||
}
|
||||
line++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
val.string.str = prop.value_str.data;
|
||||
val.string.len = prop.value_str.length + 1; /* .len needs to include null terminator. */
|
||||
val.string.subtype = IDP_STRING_SUB_UTF8;
|
||||
idprop = IDP_New(IDP_STRING, &val, name);
|
||||
}
|
||||
break;
|
||||
case UFBX_PROP_VECTOR:
|
||||
case UFBX_PROP_COLOR:
|
||||
val.array.len = 3;
|
||||
val.array.type = IDP_DOUBLE;
|
||||
idprop = IDP_New(IDP_ARRAY, &val, name);
|
||||
{
|
||||
double *dst = static_cast<double *>(idprop->data.pointer);
|
||||
dst[0] = prop.value_vec3.x;
|
||||
dst[1] = prop.value_vec3.y;
|
||||
dst[2] = prop.value_vec3.z;
|
||||
}
|
||||
break;
|
||||
case UFBX_PROP_COLOR_WITH_ALPHA:
|
||||
val.array.len = 4;
|
||||
val.array.type = IDP_DOUBLE;
|
||||
idprop = IDP_New(IDP_ARRAY, &val, name);
|
||||
{
|
||||
double *dst = static_cast<double *>(idprop->data.pointer);
|
||||
dst[0] = prop.value_vec4.x;
|
||||
dst[1] = prop.value_vec4.y;
|
||||
dst[2] = prop.value_vec4.z;
|
||||
dst[3] = prop.value_vec4.z;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (idprop != nullptr) {
|
||||
IDP_AddToGroup(idgroup, idprop);
|
||||
}
|
||||
}
|
||||
|
||||
void read_custom_properties(const ufbx_props &props, ID &id, bool enums_as_strings)
|
||||
{
|
||||
for (const ufbx_prop &prop : props.props) {
|
||||
if ((prop.flags & UFBX_PROP_FLAG_USER_DEFINED) == 0) {
|
||||
continue;
|
||||
}
|
||||
IDProperty *idgroup = IDP_EnsureProperties(&id);
|
||||
read_ufbx_property(prop, idgroup, enums_as_strings);
|
||||
}
|
||||
}
|
||||
|
||||
static IDProperty *pchan_EnsureProperties(bPoseChannel &pchan)
|
||||
{
|
||||
if (pchan.prop == nullptr) {
|
||||
pchan.prop = bke::idprop::create_group("").release();
|
||||
}
|
||||
return pchan.prop;
|
||||
}
|
||||
|
||||
void read_custom_properties(const ufbx_props &props, bPoseChannel &pchan, bool enums_as_strings)
|
||||
{
|
||||
for (const ufbx_prop &prop : props.props) {
|
||||
if ((prop.flags & UFBX_PROP_FLAG_USER_DEFINED) == 0) {
|
||||
continue;
|
||||
}
|
||||
IDProperty *idgroup = pchan_EnsureProperties(pchan);
|
||||
read_ufbx_property(prop, idgroup, enums_as_strings);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
FILE *g_debug_file;
|
||||
void print_matrix(const ufbx_matrix &m)
|
||||
{
|
||||
fprintf(g_debug_file,
|
||||
" (%.3f %.3f %.3f %.3f)\n",
|
||||
adjf(m.cols[0].x),
|
||||
adjf(m.cols[1].x),
|
||||
adjf(m.cols[2].x),
|
||||
adjf(m.cols[3].x));
|
||||
fprintf(g_debug_file,
|
||||
" (%.3f %.3f %.3f %.3f)\n",
|
||||
adjf(m.cols[0].y),
|
||||
adjf(m.cols[1].y),
|
||||
adjf(m.cols[2].y),
|
||||
adjf(m.cols[3].y));
|
||||
fprintf(g_debug_file,
|
||||
" (%.3f %.3f %.3f %.3f)\n",
|
||||
adjf(m.cols[0].z),
|
||||
adjf(m.cols[1].z),
|
||||
adjf(m.cols[2].z),
|
||||
adjf(m.cols[3].z));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
106
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.hh
Normal file
106
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.hh
Normal file
@@ -0,0 +1,106 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
#include "ufbx.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct ID;
|
||||
struct Object;
|
||||
struct Key;
|
||||
struct Material;
|
||||
struct bPoseChannel;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
const char *get_fbx_name(const ufbx_string &name, const char *def = "Untitled");
|
||||
|
||||
struct FbxElementMapping {
|
||||
Set<Object *> imported_objects;
|
||||
Map<const ufbx_element *, Object *> el_to_object;
|
||||
Map<const ufbx_element *, Key *> el_to_shape_key;
|
||||
Map<const ufbx_material *, Material *> mat_to_material;
|
||||
Map<const ufbx_node *, Object *> bone_to_armature;
|
||||
|
||||
/* For the armatures we create, for different use cases we need transform
|
||||
* from world space to the root bone, either in posed transform or in
|
||||
* node transform. */
|
||||
Map<const Object *, ufbx_matrix> armature_world_to_arm_pose_matrix;
|
||||
Map<const Object *, ufbx_matrix> armature_world_to_arm_node_matrix;
|
||||
|
||||
/* Which FBX bone nodes got turned into actual armature bones (not all of them
|
||||
* always are; in some cases root bone is the armature object itself). */
|
||||
Set<const ufbx_node *> node_is_blender_bone;
|
||||
|
||||
/* Mapping of ufbx node to object name used within blender. If names are too long
|
||||
* or duplicate, they might not match what was in FBX file. */
|
||||
Map<const ufbx_node *, std::string> node_to_name;
|
||||
/* Bone node to "bind matrix", i.e. matrix that transforms from bone (in skin bind pose) local
|
||||
* space to world space. This records bone pose or skin cluster bind matrix (skin cluster taking
|
||||
* precedence if it exists). */
|
||||
Map<const ufbx_node *, ufbx_matrix> bone_to_bind_matrix;
|
||||
Map<const ufbx_node *, ufbx_real> bone_to_length;
|
||||
Set<const ufbx_node *> bone_is_skinned;
|
||||
ufbx_matrix global_conv_matrix;
|
||||
|
||||
ufbx_matrix get_node_bind_matrix(const ufbx_node *node) const
|
||||
{
|
||||
return this->bone_to_bind_matrix.lookup_default(node, node->geometry_to_world);
|
||||
}
|
||||
|
||||
ufbx_matrix calc_local_bind_matrix(const ufbx_node *bone_node,
|
||||
const ufbx_matrix &world_to_arm) const
|
||||
{
|
||||
ufbx_matrix res = this->get_node_bind_matrix(bone_node);
|
||||
ufbx_matrix parent_inv_mtx;
|
||||
if (bone_node->parent != nullptr && !bone_node->parent->is_root) {
|
||||
ufbx_matrix parent_mtx = this->get_node_bind_matrix(bone_node->parent);
|
||||
parent_inv_mtx = ufbx_matrix_invert(&parent_mtx);
|
||||
}
|
||||
else {
|
||||
parent_inv_mtx = world_to_arm;
|
||||
}
|
||||
res = ufbx_matrix_mul(&parent_inv_mtx, &res);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
void matrix_to_m44(const ufbx_matrix &src, float dst[4][4]);
|
||||
void ufbx_matrix_to_obj(const ufbx_matrix &mtx, Object *obj);
|
||||
void node_matrix_to_obj(const ufbx_node *node, Object *obj, const FbxElementMapping &mapping);
|
||||
void read_custom_properties(const ufbx_props &props, ID &id, bool enums_as_strings);
|
||||
void read_custom_properties(const ufbx_props &props, bPoseChannel &pchan, bool enums_as_strings);
|
||||
|
||||
ufbx_matrix calc_bone_pose_matrix(const ufbx_transform &local_xform,
|
||||
const ufbx_node &node,
|
||||
const ufbx_matrix &local_bind_inv_matrix);
|
||||
|
||||
//@TODO remove debug file print once things are working properly
|
||||
// #define FBX_DEBUG_PRINT
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
extern FILE *g_debug_file;
|
||||
|
||||
inline double adjf(double f)
|
||||
{
|
||||
if (fabs(f) < 0.0005) {
|
||||
return 0.0;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
void print_matrix(const ufbx_matrix &m);
|
||||
#endif
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
Reference in New Issue
Block a user