Selaa lähdekoodia

ufbx: Update to 0.18.0

Jakub Marcowski 4 kuukautta sitten
vanhempi
commit
533daa5552
3 muutettua tiedostoa jossa 433 lisäystä ja 64 poistoa
  1. 1 1
      thirdparty/README.md
  2. 357 60
      thirdparty/ufbx/ufbx.c
  3. 75 3
      thirdparty/ufbx/ufbx.h

+ 1 - 1
thirdparty/README.md

@@ -974,7 +974,7 @@ Patches:
 ## ufbx
 
 - Upstream: https://github.com/ufbx/ufbx
-- Version: 0.17.1 (6ca5309972f03625e6990f3084ff4c1cc55a09b6, 2025)
+- Version: 0.18.0 (729ab835444f5f229e5f7cff332692ce6c00415d, 2025)
 - License: MIT
 
 Files extracted from upstream source:

+ 357 - 60
thirdparty/ufbx/ufbx.c

@@ -270,6 +270,7 @@
 	#define ufbx_fmax ufbxi_math_fn(fmax)
 	#define ufbx_nextafter ufbxi_math_fn(nextafter)
 	#define ufbx_rint ufbxi_math_fn(rint)
+	#define ufbx_floor ufbxi_math_fn(floor)
 	#define ufbx_ceil ufbxi_math_fn(ceil)
 	#define ufbx_isnan ufbxi_math_fn(isnan)
 #endif
@@ -296,6 +297,7 @@ extern "C" {
 	ufbx_extern_abi double ufbx_copysign(double x, double y);
 	ufbx_extern_abi double ufbx_nextafter(double x, double y);
 	ufbx_extern_abi double ufbx_rint(double x);
+	ufbx_extern_abi double ufbx_floor(double x);
 	ufbx_extern_abi double ufbx_ceil(double x);
 	ufbx_extern_abi int ufbx_isnan(double x);
 #endif
@@ -532,6 +534,10 @@ extern "C" {
 			#pragma GCC diagnostic ignored "-Wc99-c11-compat"
 		#endif
 	#endif
+	// MSC isnan() definition triggers this error on MinGW GCC
+	#if defined(__MINGW32__)
+		#pragma GCC diagnostic ignored "-Wfloat-conversion"
+	#endif
 #endif
 
 #if !defined(ufbx_static_assert)
@@ -830,7 +836,7 @@ ufbx_static_assert(sizeof_f64, sizeof(double) == 8);
 
 // -- Version
 
-#define UFBX_SOURCE_VERSION ufbx_pack_version(0, 17, 1)
+#define UFBX_SOURCE_VERSION ufbx_pack_version(0, 18, 0)
 ufbx_abi_data_def const uint32_t ufbx_source_version = UFBX_SOURCE_VERSION;
 
 ufbx_static_assert(source_header_version, UFBX_SOURCE_VERSION/1000u == UFBX_HEADER_VERSION/1000u);
@@ -1656,7 +1662,7 @@ static ufbxi_noinline double ufbxi_parse_double(const char *str, size_t max_leng
 	}
 }
 
-static ufbxi_noinline uint32_t ufbxi_parse_double_init_flags()
+static ufbxi_noinline uint32_t ufbxi_parse_double_init_flags(void)
 {
 	// We require evaluation in double precision, either for doubles (0) or always (1)
 	// and rounding to nearest, which we can check for with `1 + eps == 1 - eps`.
@@ -5198,6 +5204,7 @@ static const char ufbxi_Edges[] = "Edges";
 static const char ufbxi_EmissiveColor[] = "EmissiveColor";
 static const char ufbxi_Entry[] = "Entry";
 static const char ufbxi_FBXHeaderExtension[] = "FBXHeaderExtension";
+static const char ufbxi_FBXHeaderVersion[] = "FBXHeaderVersion";
 static const char ufbxi_FBXVersion[] = "FBXVersion";
 static const char ufbxi_FKEffector[] = "FKEffector";
 static const char ufbxi_FarPlane[] = "FarPlane";
@@ -5307,6 +5314,7 @@ static const char ufbxi_OriginalUnitScaleFactor[] = "OriginalUnitScaleFactor";
 static const char ufbxi_OriginalUpAxis[] = "OriginalUpAxis";
 static const char ufbxi_OriginalUpAxisSign[] = "OriginalUpAxisSign";
 static const char ufbxi_OrthoZoom[] = "OrthoZoom";
+static const char ufbxi_OtherFlags[] = "OtherFlags";
 static const char ufbxi_OuterAngle[] = "OuterAngle";
 static const char ufbxi_PO[] = "PO\0";
 static const char ufbxi_PP[] = "PP\0";
@@ -5317,7 +5325,9 @@ static const char ufbxi_PolygonIndexArray[] = "PolygonIndexArray";
 static const char ufbxi_PolygonVertexIndex[] = "PolygonVertexIndex";
 static const char ufbxi_PoseNode[] = "PoseNode";
 static const char ufbxi_Pose[] = "Pose";
+static const char ufbxi_Post_Extrapolation[] = "Post-Extrapolation";
 static const char ufbxi_PostRotation[] = "PostRotation";
+static const char ufbxi_Pre_Extrapolation[] = "Pre-Extrapolation";
 static const char ufbxi_PreRotation[] = "PreRotation";
 static const char ufbxi_PreviewDivisionLevels[] = "PreviewDivisionLevels";
 static const char ufbxi_Properties60[] = "Properties60";
@@ -5330,6 +5340,7 @@ static const char ufbxi_ReferenceTime[] = "ReferenceTime";
 static const char ufbxi_RelativeFileName[] = "RelativeFileName";
 static const char ufbxi_RelativeFilename[] = "RelativeFilename";
 static const char ufbxi_RenderDivisionLevels[] = "RenderDivisionLevels";
+static const char ufbxi_Repetition[] = "Repetition";
 static const char ufbxi_RightCamera[] = "RightCamera";
 static const char ufbxi_RootNode[] = "RootNode";
 static const char ufbxi_Root[] = "Root";
@@ -5360,6 +5371,7 @@ static const char ufbxi_SpecularColor[] = "SpecularColor";
 static const char ufbxi_Step[] = "Step";
 static const char ufbxi_SubDeformer[] = "SubDeformer";
 static const char ufbxi_T[] = "T\0\0";
+static const char ufbxi_TCDefinition[] = "TCDefinition";
 static const char ufbxi_Take[] = "Take";
 static const char ufbxi_Takes[] = "Takes";
 static const char ufbxi_Tangents[] = "Tangents";
@@ -5493,6 +5505,7 @@ static const ufbx_string ufbxi_strings[] = {
 	{ ufbxi_EmissiveColor, 13 },
 	{ ufbxi_Entry, 5 },
 	{ ufbxi_FBXHeaderExtension, 18 },
+	{ ufbxi_FBXHeaderVersion, 16 },
 	{ ufbxi_FBXVersion, 10 },
 	{ ufbxi_FKEffector, 10 },
 	{ ufbxi_FarPlane, 8 },
@@ -5602,6 +5615,7 @@ static const ufbx_string ufbxi_strings[] = {
 	{ ufbxi_OriginalUpAxis, 14 },
 	{ ufbxi_OriginalUpAxisSign, 18 },
 	{ ufbxi_OrthoZoom, 9 },
+	{ ufbxi_OtherFlags, 10 },
 	{ ufbxi_OuterAngle, 10 },
 	{ ufbxi_PO, 2 },
 	{ ufbxi_PP, 2 },
@@ -5612,7 +5626,9 @@ static const ufbx_string ufbxi_strings[] = {
 	{ ufbxi_PolygonVertexIndex, 18 },
 	{ ufbxi_Pose, 4 },
 	{ ufbxi_PoseNode, 8 },
+	{ ufbxi_Post_Extrapolation, 18 },
 	{ ufbxi_PostRotation, 12 },
+	{ ufbxi_Pre_Extrapolation, 17 },
 	{ ufbxi_PreRotation, 11 },
 	{ ufbxi_PreviewDivisionLevels, 21 },
 	{ ufbxi_Properties60, 12 },
@@ -5625,6 +5641,7 @@ static const ufbx_string ufbxi_strings[] = {
 	{ ufbxi_RelativeFileName, 16 },
 	{ ufbxi_RelativeFilename, 16 },
 	{ ufbxi_RenderDivisionLevels, 20 },
+	{ ufbxi_Repetition, 10 },
 	{ ufbxi_RightCamera, 11 },
 	{ ufbxi_Root, 4 },
 	{ ufbxi_RootNode, 8 },
@@ -5655,6 +5672,7 @@ static const ufbx_string ufbxi_strings[] = {
 	{ ufbxi_Step, 4 },
 	{ ufbxi_SubDeformer, 11 },
 	{ ufbxi_T, 1 },
+	{ ufbxi_TCDefinition, 12 },
 	{ ufbxi_Take, 4 },
 	{ ufbxi_Takes, 5 },
 	{ ufbxi_Tangents, 8 },
@@ -7506,11 +7524,16 @@ static ufbxi_noinline ufbxi_node *ufbxi_find_child(ufbxi_node *node, const char
 	return NULL;
 }
 
+// Retrieve the type of a given value
+ufbxi_forceinline static ufbxi_value_type ufbxi_get_val_type(ufbxi_node *node, size_t ix)
+{
+	return (ufbxi_value_type)((node->value_type_mask >> (ix*2)) & 0x3);
+}
+
 // Retrieve values from nodes with type codes:
 // Any: '_' (ignore)
 // NUMBER: 'I' int32_t 'L' int64_t 'F' float 'D' double 'R' ufbxi_real 'B' bool 'Z' size_t
 // STRING: 'S' ufbx_string 'C' const char* (checked) 's' ufbx_string 'c' const char * (unchecked) 'b' ufbx_blob
-
 ufbxi_nodiscard ufbxi_forceinline static int ufbxi_get_val_at(ufbxi_node *node, size_t ix, char fmt, void *v)
 {
 	ufbxi_dev_assert(ix < UFBXI_MAX_NON_ARRAY_VALUES);
@@ -9542,7 +9565,7 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_next_token(ufbxi_context *
 	if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') {
 		token->type = UFBXI_ASCII_BARE_WORD;
 		while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
-			|| (c >= '0' && c <= '9') || c == '_') {
+			|| (c >= '0' && c <= '9') || c == '_' || c == '-') {
 			ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c));
 			c = ufbxi_ascii_next(uc);
 		}
@@ -11216,6 +11239,7 @@ static const ufbxi_prop_type_name ufbxi_prop_type_names[] = {
 	{ "Integer", UFBX_PROP_INTEGER },
 	{ "int", UFBX_PROP_INTEGER },
 	{ "enum", UFBX_PROP_INTEGER },
+	{ "Enum", UFBX_PROP_INTEGER },
 	{ "Visibility", UFBX_PROP_INTEGER },
 	{ "Visibility Inheritance", UFBX_PROP_INTEGER },
 	{ "KTime", UFBX_PROP_INTEGER },
@@ -11498,7 +11522,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_init_node_prop_names(ufbxi_conte
 	return 1;
 }
 
-static bool ufbxi_is_node_property(ufbxi_context *uc, const char *name)
+static bool ufbxi_is_node_property_name(ufbxi_context *uc, const char *name)
 {
 	// You need to call `ufbxi_init_node_prop_names()` before calling this
 	ufbx_assert(uc->node_prop_set.size > 0);
@@ -11604,8 +11628,11 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_property(ufbxi_context *uc,
 		flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_REAL << (real_ix - 1);
 	}
 
-	// Distance properties have a string unit _after_ the real value, eg. `10, "cm"`
-	if (prop->type == UFBX_PROP_DISTANCE) {
+	// Skip one value forward in case the current value is not a string, as some properties
+	// contain mixed numbers and strings. Currenltly known cases:
+	//   Lod Distance:    P: "Thresholds|Level0", "Distance", "", "",64, "cm"
+	//   User Enum:       P: "User_Enum", "Enum", "", "A+U",1, "ValueA~ValueB~ValueC"
+	if (ufbxi_get_val_type(node, val_ix) != UFBXI_VALUE_STRING) {
 		val_ix++;
 	}
 
@@ -11742,9 +11769,9 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_scene_info(ufbxi_context *u
 
 ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_header_extension(ufbxi_context *uc)
 {
-	// TODO: Read TCDefinition and adjust timestamps
-	uc->ktime_sec = 46186158000;
-	uc->ktime_sec_double = (double)uc->ktime_sec;
+	bool has_tc_definition = false;
+	int32_t tc_definition = 0;
+	int32_t header_version = 0;
 
 	for (;;) {
 		ufbxi_node *child;
@@ -11764,12 +11791,33 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_header_extension(ufbxi_cont
 			}
 		}
 
+		if (child->name == ufbxi_FBXHeaderVersion) {
+			ufbxi_ignore(ufbxi_get_val1(child, "I", &header_version));
+		}
+
+		if (child->name == ufbxi_OtherFlags) {
+			if (ufbxi_find_val1(child, ufbxi_TCDefinition, "I", &tc_definition)) {
+				has_tc_definition = true;
+			}
+		}
+
 		if (child->name == ufbxi_SceneInfo) {
 			ufbxi_check(ufbxi_read_scene_info(uc, child));
 		}
 
 	}
 
+	// FBX 8000 will change the KTime units and the new units are opt-in currently via `TCDefinition`.
+	// `TCDefinition` seems be accounted in all versions, as long as `FBXHeaderVersion >= 1004`.
+	// The old KTime units are specified as the value `127` and all other values seem to use the new definition.
+	bool use_v7_ktime = uc->version < 8000;
+	if (header_version >= 1004 && has_tc_definition) {
+		use_v7_ktime = tc_definition == 127;
+	}
+
+	uc->ktime_sec = use_v7_ktime ? 46186158000 : 141120000;
+	uc->ktime_sec_double = (double)uc->ktime_sec;
+
 	return 1;
 }
 
@@ -13935,11 +13983,44 @@ static void ufbxi_solve_tcb(float *p_slope_left, float *p_slope_right, double te
 	*p_slope_right = (float)(d10 * slope_left + d11 * slope_right);
 }
 
+ufbxi_noinline static void ufbxi_read_extrapolation(ufbx_extrapolation *p_extrapolation, ufbxi_node *node, const char *name)
+{
+	ufbxi_node *child = ufbxi_find_child(node, name);
+	ufbx_extrapolation_mode mode = UFBX_EXTRAPOLATION_CONSTANT;
+	int32_t repeat_count = -1;
+
+	if (child) {
+		int32_t mode_ch;
+		if (ufbxi_find_val1(child, ufbxi_Type, "I", &mode_ch)) {
+
+			switch (mode_ch) {
+			case 'A': mode = UFBX_EXTRAPOLATION_REPEAT_RELATIVE; break;
+			case 'C': mode = UFBX_EXTRAPOLATION_CONSTANT; break;
+			case 'K': mode = UFBX_EXTRAPOLATION_SLOPE; break;
+			case 'M': mode = UFBX_EXTRAPOLATION_MIRROR; break;
+			case 'R': mode = UFBX_EXTRAPOLATION_REPEAT; break;
+			default: /* Unknown */ break;
+			}
+			if (ufbxi_find_val1(child, ufbxi_Repetition, "I", &repeat_count)) {
+				if (repeat_count < 0) {
+					repeat_count = -1;
+				}
+			}
+		}
+	}
+
+	p_extrapolation->mode = mode;
+	p_extrapolation->repeat_count = repeat_count;
+}
+
 ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_animation_curve(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info)
 {
 	ufbx_anim_curve *curve = ufbxi_push_element(uc, info, ufbx_anim_curve, UFBX_ELEMENT_ANIM_CURVE);
 	ufbxi_check(curve);
 
+	ufbxi_read_extrapolation(&curve->pre_extrapolation, node, ufbxi_Pre_Extrapolation);
+	ufbxi_read_extrapolation(&curve->post_extrapolation, node, ufbxi_Post_Extrapolation);
+
 	if (uc->opts.ignore_animation) return 1;
 
 	ufbxi_value_array *times, *values, *attr_flags, *attrs, *refs;
@@ -14537,27 +14618,26 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_synthetic_attribute(ufbxi_c
 
 	// 6x00: Link the node to the node attribute so property connections can be
 	// redirected from connections if necessary.
-	if (uc->version < 7000) {
-		ufbxi_check(ufbxi_insert_fbx_attr(uc, info->fbx_id, attrib_info.fbx_id));
-
-		// Split properties between the node and the attribute
-		ufbx_prop *ps = info->props.props.data;
-		size_t dst = 0, src = 0, end = info->props.props.count;
-		while (src < end) {
-			if (!ufbxi_is_node_property(uc, ps[src].name.data)) {
-				ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, 1, &ps[src]));
-				src++;
-			} else if (dst != src) {
-				ps[dst++] = ps[src++];
-			} else {
-				dst++; src++;
-			}
+	ufbxi_check(ufbxi_insert_fbx_attr(uc, info->fbx_id, attrib_info.fbx_id));
+
+	// Split properties between the node and the attribute.
+	// Consider all user properties as node properties.
+	ufbx_prop *ps = info->props.props.data;
+	size_t dst = 0, src = 0, end = info->props.props.count;
+	while (src < end) {
+		if (!ufbxi_is_node_property_name(uc, ps[src].name.data) && (ps[src].flags & UFBX_PROP_FLAG_USER_DEFINED) == 0) {
+			ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, 1, &ps[src]));
+			src++;
+		} else if (dst != src) {
+			ps[dst++] = ps[src++];
+		} else {
+			dst++; src++;
 		}
-		attrib_info.props.props.count = end - dst;
-		attrib_info.props.props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_prop, attrib_info.props.props.count);
-		ufbxi_check(attrib_info.props.props.data);
-		info->props.props.count = dst;
 	}
+	attrib_info.props.props.count = end - dst;
+	attrib_info.props.props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_prop, attrib_info.props.props.count);
+	ufbxi_check(attrib_info.props.props.data);
+	info->props.props.count = dst;
 
 	if (sub_type == ufbxi_Mesh) {
 		ufbxi_check(ufbxi_read_mesh(uc, node, &attrib_info));
@@ -14995,6 +15075,9 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_anim_channel(ufbxi_con
 
 	ufbxi_check(ufbxi_connect_op(uc, curve_fbx_id, value_fbx_id, curve->name));
 
+	ufbxi_read_extrapolation(&curve->pre_extrapolation, node, ufbxi_Pre_Extrapolation);
+	ufbxi_read_extrapolation(&curve->post_extrapolation, node, ufbxi_Post_Extrapolation);
+
 	if (uc->opts.ignore_animation) return 1;
 
 	size_t num_keys = 0;
@@ -15440,6 +15523,11 @@ ufbxi_noinline static void ufbxi_setup_root_node(ufbxi_context *uc, ufbx_node *r
 	root->is_root = true;
 }
 
+static ufbxi_forceinline bool ufbxi_supports_version(uint32_t version)
+{
+	return version >= 3000 && version <= 7700;
+}
+
 ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_root(ufbxi_context *uc)
 {
 	// FBXHeaderExtension: Some metadata (optional)
@@ -17335,6 +17423,8 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_file(ufbxi_context *uc
 			uc->obj.mtllib_relative_path.size = lib.length;
 		} else if (ufbxi_str_equal(cmd, ufbxi_str_c("usemtl"))) {
 			ufbxi_check(ufbxi_obj_parse_material(uc));
+		} else if (!uc->opts.disable_quirks && key == 0) {
+			// ZBrush exporter seems to end the files with '\0', sometimes..
 		} else {
 			ufbxi_check(ufbxi_warnf(UFBX_WARNING_UNKNOWN_OBJ_DIRECTIVE, "Unknown .obj directive, skipped line"));
 		}
@@ -18168,15 +18258,21 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_connections(ufbxi_contex
 	uc->scene.connections_src.data = ufbxi_push(&uc->result, ufbx_connection, num_connections);
 	ufbxi_check(uc->scene.connections_src.data);
 
-	// HACK: Translate property connections from node to attribute if
-	// the property name is not included in the known node properties.
+	// HACK: Translate property connections from node to attribute if the property name is not included
+	// in the known node properties and is not a property of the node.
 	if (uc->version > 0 && uc->version < 7000) {
 		ufbxi_for(ufbxi_tmp_connection, tmp_conn, tmp_connections, num_connections) {
-			if (tmp_conn->src_prop.length > 0 && !ufbxi_is_node_property(uc, tmp_conn->src_prop.data)) {
-				tmp_conn->src = ufbxi_find_attribute_fbx_id(uc, tmp_conn->src);
+			if (tmp_conn->src_prop.length > 0 && !ufbxi_is_node_property_name(uc, tmp_conn->src_prop.data)) {
+				ufbx_element *src = ufbxi_find_element_by_fbx_id(uc, tmp_conn->src);
+				if (!src || !ufbx_find_prop_len(&src->props, tmp_conn->src_prop.data, tmp_conn->src_prop.length)) {
+					tmp_conn->src = ufbxi_find_attribute_fbx_id(uc, tmp_conn->src);
+				}
 			}
-			if (tmp_conn->dst_prop.length > 0 && !ufbxi_is_node_property(uc, tmp_conn->dst_prop.data)) {
-				tmp_conn->dst = ufbxi_find_attribute_fbx_id(uc, tmp_conn->dst);
+			if (tmp_conn->dst_prop.length > 0 && !ufbxi_is_node_property_name(uc, tmp_conn->dst_prop.data)) {
+				ufbx_element *dst = ufbxi_find_element_by_fbx_id(uc, tmp_conn->dst);
+				if (!dst || !ufbx_find_prop_len(&dst->props, tmp_conn->dst_prop.data, tmp_conn->dst_prop.length)) {
+					tmp_conn->dst = ufbxi_find_attribute_fbx_id(uc, tmp_conn->dst);
+				}
 			}
 		}
 	}
@@ -18882,6 +18978,8 @@ typedef enum {
 	UFBXI_SHADER_MAPPING_DEFAULT_W_1 = 0x1,
 	// Widen values to RGB if only a single value is present.
 	UFBXI_SHADER_MAPPING_WIDEN_TO_RGB = 0x2,
+	// Multiply the existing value.
+	UFBXI_SHADER_MAPPING_MULTIPLY_VALUE = 0x4,
 } ufbxi_shader_mapping_flag;
 
 typedef enum {
@@ -19187,6 +19285,56 @@ static const ufbxi_shader_mapping ufbxi_gltf_material_pbr_mapping[] = {
 	{ UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("extension|indexOfRefraction") },
 };
 
+static const ufbxi_shader_mapping ufbxi_openpbr_material_pbr_mapping[] = {
+	{ UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base_weight") },
+	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") },
+	{ UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("specular_roughness") },
+	{ UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("base_diffuse_roughness") },
+	{ UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("base_metalness") },
+	{ UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("specular_weight") },
+	{ UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specular_color") },
+	{ UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("specular_roughness_anisotropy") },
+	{ UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("specular_ior") },
+	{ UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transmission_weight") },
+	{ UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("transmission_color") },
+	{ UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("transmission_depth") },
+	{ UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("transmission_scatter") },
+	{ UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER_ANISOTROPY, 0, 0, ufbxi_mat_string("transmission_scatter_anisotropy") },
+	{ UFBX_MATERIAL_PBR_TRANSMISSION_DISPERSION, 0, 0, ufbxi_mat_string("transmission_dispersion_scale") },
+	{ UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("subsurface_weight") },
+	{ UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("subsurface_color") },
+	{ UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("subsurface_radius_scale") },
+	{ UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("subsurface_radius") },
+	{ UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, 0, 0, ufbxi_mat_string("subsurface_scatter_anisotropy") },
+	{ UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat_weight") },
+	{ UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_color") },
+	{ UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_roughness") },
+	{ UFBX_MATERIAL_PBR_COAT_ANISOTROPY, 0, 0, ufbxi_mat_string("coat_roughness_anisotropy") },
+	{ UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coat_ior") },
+	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_normal_map") },
+	{ UFBX_MATERIAL_PBR_SHEEN_FACTOR, 0, 0, ufbxi_mat_string("fuzz_weight") },
+	{ UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("fuzz_color") },
+	{ UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("fuzz_roughness") },
+	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission_weight") },
+	{ UFBX_MATERIAL_PBR_EMISSION_FACTOR, UFBXI_SHADER_MAPPING_MULTIPLY_VALUE, 0, ufbxi_mat_string("emission_luminance") },
+	{ UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emission_color") },
+	{ UFBX_MATERIAL_PBR_THIN_FILM_FACTOR, 0, 0, ufbxi_mat_string("thin_film_weight") },
+	{ UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, 0, 0, ufbxi_mat_string("thin_film_thickness") },
+	{ UFBX_MATERIAL_PBR_THIN_FILM_IOR, 0, 0, ufbxi_mat_string("thin_film_ior") },
+	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump") },
+	{ UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump_map_amt") },
+	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") },
+	{ UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_map_amt") },
+	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_bump") },
+	{ UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_bump_map_amt") },
+	{ UFBX_MATERIAL_PBR_TANGENT_MAP, 0, 0, ufbxi_mat_string("geometry_tangent_map") },
+	{ UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("geometry_opacity") },
+};
+
+static const ufbxi_shader_mapping ufbxi_openpbr_material_features[] = {
+	{ UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("geometry_thin_walled") },
+};
+
 static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_metal_rough_pbr_mapping[] = {
 	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") },
 	{ UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("baseColor") },
@@ -19365,6 +19513,14 @@ static const ufbxi_shader_mapping_list ufbxi_shader_pbr_mappings[] = {
 		{ NULL, 0 }, ufbxi_string_literal("Map"), // texture_prefix/suffix
 		{ NULL, 0 }, { NULL, 0 }, // texture_enabled_prefix/suffix
 	},
+	{ // UFBX_SHADER_OPENPBR_MATERIAL
+		ufbxi_openpbr_material_pbr_mapping, ufbxi_arraycount(ufbxi_openpbr_material_pbr_mapping),
+		ufbxi_openpbr_material_features, ufbxi_arraycount(ufbxi_openpbr_material_features),
+		(uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_COAT
+			| UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR | UFBXI_MAT_DIFFUSE_ROUGHNESS),
+		{ NULL, 0 }, ufbxi_string_literal("_map"),    // texture_prefix/suffix
+		{ NULL, 0 }, ufbxi_string_literal("_map_on"), // texture_enabled_prefix/suffix
+	},
 	{ // UFBX_SHADER_SHADERFX_GRAPH
 		ufbxi_shaderfx_graph_pbr_mapping, ufbxi_arraycount(ufbxi_shaderfx_graph_pbr_mapping),
 		NULL, 0,
@@ -19468,8 +19624,13 @@ ufbxi_noinline static void ufbxi_fetch_mapping_maps(ufbx_material *material, ufb
 
 			if (flags & UFBXI_MAPPING_FETCH_VALUE) {
 				if (prop && prop->type != UFBX_PROP_REFERENCE) {
-					map->value_vec4 = prop->value_vec4;
-					map->value_int = prop->value_int;
+					if ((mapping->flags & UFBXI_SHADER_MAPPING_MULTIPLY_VALUE) != 0) {
+						map->value_vec4.x *= prop->value_vec4.x;
+						map->value_int = ufbxi_f64_to_i64(map->value_vec4.x);
+					} else {
+						map->value_vec4 = prop->value_vec4;
+						map->value_int = prop->value_int;
+					}
 					map->has_value = true;
 					if (mapping->transform) {
 						ufbxi_mat_transform_fn transform_fn = ufbxi_mat_transform_fns[mapping->transform];
@@ -19611,6 +19772,7 @@ ufbxi_noinline static void ufbxi_fetch_maps(ufbx_scene *scene, ufbx_material *ma
 	ufbxi_update_factor(&material->pbr.specular_factor, &material->pbr.specular_color);
 	ufbxi_update_factor(&material->pbr.emission_factor, &material->pbr.emission_color);
 	ufbxi_update_factor(&material->pbr.sheen_factor, &material->pbr.sheen_color);
+	ufbxi_update_factor(&material->pbr.thin_film_factor, &material->pbr.thin_film_thickness);
 	ufbxi_update_factor(&material->pbr.transmission_factor, &material->pbr.transmission_color);
 
 	// Patch transmission roughness if only extra roughness is defined
@@ -19909,6 +20071,7 @@ static const ufbxi_file_shader ufbxi_file_shaders[] = {
 	{ UINT64_C(0x7e73161fad53b12a), "ai_image", "filename" },
 	{ 0, "OSLBitmap", ufbxi_Filename },
 	{ 0, "OSLBitmap2", ufbxi_Filename },
+	{ 0, "OSLBitmap3", ufbxi_Filename },
 	{ 0, "UberBitmap", ufbxi_Filename },
 	{ 0, "UberBitmap2", ufbxi_Filename },
 };
@@ -21697,6 +21860,14 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc
 		}
 	}
 
+	ufbxi_for_ptr_list(ufbx_anim_curve, p_curve, uc->scene.anim_curves) {
+		ufbx_anim_curve *curve = *p_curve;
+		if (curve->keyframes.count > 0) {
+			curve->min_time = curve->keyframes.data[0].time;
+			curve->max_time = curve->keyframes.data[curve->keyframes.count - 1].time;
+		}
+	}
+
 	ufbxi_for_ptr_list(ufbx_shader, p_shader, uc->scene.shaders) {
 		ufbx_shader *shader = *p_shader;
 		ufbxi_check(ufbxi_fetch_dst_elements(uc, &shader->bindings, &shader->element, false, false, NULL, UFBX_ELEMENT_SHADER_BINDING));
@@ -21738,6 +21909,10 @@ ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc
 					material->shader_type = UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL;
 					material->shader_prop_prefix.data = "3dsMax|Parameters|";
 					material->shader_prop_prefix.length = strlen("3dsMax|Parameters|");
+				} else if (classid_a == 0xf1551e33u && classid_b == 0x37fb1337u) {
+					material->shader_type = UFBX_SHADER_OPENPBR_MATERIAL;
+					material->shader_prop_prefix.data = "3dsMax|Parameters|";
+					material->shader_prop_prefix.length = strlen("3dsMax|Parameters|");
 				} else if (classid_a == 0x38420192u && classid_b == 0x45fe4e1bu) {
 					material->shader_type = UFBX_SHADER_GLTF_MATERIAL;
 					material->shader_prop_prefix.data = "3dsMax|";
@@ -24671,6 +24846,9 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_imp(ufbxi_context *uc)
 		} else {
 			ufbxi_check(ufbxi_read_root(uc));
 		}
+		if (!ufbxi_supports_version(uc->version)) {
+			ufbxi_check(ufbxi_warnf(UFBX_WARNING_UNSUPPORTED_VERSION, "Unsupported FBX version (%u)", uc->version));
+		}
 		ufbxi_update_scene_metadata(&uc->scene.metadata);
 		ufbxi_check(ufbxi_init_file_paths(uc));
 	} else if (format == UFBX_FILE_FORMAT_OBJ) {
@@ -24978,6 +25156,12 @@ static ufbxi_noinline ufbx_scene *ufbxi_load(ufbxi_context *uc, const ufbx_load_
 		return &uc->scene_imp->scene;
 	} else {
 		ufbxi_fix_error_type(&uc->error, "Failed to load", p_error);
+		if (p_error && p_error->type == UFBX_ERROR_UNKNOWN && uc->scene.metadata.file_format == UFBX_FILE_FORMAT_FBX && !ufbxi_supports_version(uc->version)) {
+			p_error->description.data = "Unsupported version";
+			p_error->description.length = strlen("Unsupported version");
+			p_error->type = UFBX_ERROR_UNSUPPORTED_VERSION;
+			ufbxi_fmt_err_info(p_error, "%u", uc->version);
+		}
 		ufbxi_free_result(uc);
 		return NULL;
 	}
@@ -25115,7 +25299,7 @@ static ufbxi_forceinline bool ufbxi_anim_layer_might_contain_id(const ufbx_anim_
 	return ok;
 }
 
-static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, size_t num_props)
+static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, size_t num_props, uint32_t flags)
 {
 	ufbxi_anim_layer_combine_ctx combine_ctx = { anim, element, time };
 
@@ -25131,7 +25315,7 @@ static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufb
 		if (layer->weight_is_animated && layer->blended) {
 			ufbx_anim_prop *weight_aprop = ufbxi_find_anim_prop_start(layer, &layer->element);
 			if (weight_aprop) {
-				weight = ufbx_evaluate_anim_value_real(weight_aprop->anim_value, time) / (ufbx_real)100.0;
+				weight = ufbx_evaluate_anim_value_real_flags(weight_aprop->anim_value, time, flags) / (ufbx_real)100.0;
 				if (weight < 0.0f) weight = 0.0f;
 				if (weight > 0.99999f) weight = 1.0f;
 			}
@@ -25160,7 +25344,7 @@ static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufb
 			// This could be done by having `UFBX_PROP_FLAG_ANIMATION_EVALUATED`
 			// that gets set for the first layer of animation that is applied.
 			if (aprop->prop_name.data == prop->name.data) {
-				ufbx_vec3 v = ufbx_evaluate_anim_value_vec3(aprop->anim_value, time);
+				ufbx_vec3 v = ufbx_evaluate_anim_value_vec3_flags(aprop->anim_value, time, flags);
 				if (layer_ix == 0) {
 					prop->value_vec3 = v;
 				} else {
@@ -25178,9 +25362,9 @@ static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufb
 
 // Recursion limited by not calling `ufbx_evaluate_prop_len()` with a connected property,
 // meaning it will never call `ufbxi_evaluate_connected_prop()` again indirectly.
-static ufbxi_noinline void ufbxi_evaluate_connected_prop(ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time)
-	ufbxi_recursive_function_void(ufbxi_evaluate_connected_prop, (prop, anim, element, name, time), 3,
-		(ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time))
+static ufbxi_noinline void ufbxi_evaluate_connected_prop(ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags)
+	ufbxi_recursive_function_void(ufbxi_evaluate_connected_prop, (prop, anim, element, name, time, flags), 3,
+		(ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags))
 {
 	ufbx_connection *conn = ufbxi_find_prop_connection(element, name);
 
@@ -25192,7 +25376,7 @@ static ufbxi_noinline void ufbxi_evaluate_connected_prop(ufbx_prop *prop, const
 
 	// Found a non-cyclic connection
 	if (conn && !ufbxi_find_prop_connection(conn->src, conn->src_prop.data)) {
-		ufbx_prop ep = ufbx_evaluate_prop_len(anim, conn->src, conn->src_prop.data, conn->src_prop.length, time);
+		ufbx_prop ep = ufbx_evaluate_prop_len_flags(anim, conn->src, conn->src_prop.data, conn->src_prop.length, time, flags);
 		prop->value_vec4 = ep.value_vec4;
 		prop->value_int = ep.value_int;
 		prop->value_str = ep.value_str;
@@ -25282,7 +25466,7 @@ static ufbxi_forceinline const ufbx_prop *ufbxi_next_prop(ufbxi_prop_iter *iter)
 	}
 }
 
-static ufbxi_noinline ufbx_props ufbxi_evaluate_selected_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, const char *const *prop_names, size_t max_props)
+static ufbxi_noinline ufbx_props ufbxi_evaluate_selected_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, const char *const *prop_names, size_t max_props, uint32_t flags)
 {
 	const char *name = prop_names[0];
 	uint32_t key = ufbxi_get_name_key_c(name);
@@ -25306,7 +25490,7 @@ static ufbxi_noinline ufbx_props ufbxi_evaluate_selected_props(const ufbx_anim *
 				if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) {
 					ufbx_prop *dst = &props[num_props++];
 					*dst = *prop;
-					ufbxi_evaluate_connected_prop(dst, anim, element, name, time);
+					ufbxi_evaluate_connected_prop(dst, anim, element, name, time, flags);
 				} else if ((prop->flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_OVERRIDDEN)) != 0) {
 					props[num_props++] = *prop;
 				}
@@ -25323,7 +25507,7 @@ static ufbxi_noinline ufbx_props ufbxi_evaluate_selected_props(const ufbx_anim *
 		}
 	}
 
-	ufbxi_evaluate_props(anim, element, time, props, num_props);
+	ufbxi_evaluate_props(anim, element, time, props, num_props, flags);
 
 	ufbx_props prop_list;
 	prop_list.props.data = props;
@@ -25332,6 +25516,74 @@ static ufbxi_noinline ufbx_props ufbxi_evaluate_selected_props(const ufbx_anim *
 	return prop_list;
 }
 
+// Recursion limited by not calling `ufbx_evaluate_curve()` with `UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION`.
+static ufbxi_noinline ufbx_real ufbxi_extrapolate_curve(const ufbx_anim_curve *curve, double real_time, uint32_t flags)
+	ufbxi_recursive_function(ufbx_real, ufbxi_extrapolate_curve, (curve, real_time, flags), 3,
+		(const ufbx_anim_curve *curve, double real_time, uint32_t flags))
+{
+	bool pre = real_time < curve->min_time;
+	const ufbx_keyframe *key;
+	ufbx_extrapolation ext;
+	if (pre) {
+		key = &curve->keyframes.data[0];
+		ext = curve->pre_extrapolation;
+	} else {
+		key = &curve->keyframes.data[curve->keyframes.count - 1];
+		ext = curve->post_extrapolation;
+	}
+
+	if (ext.mode == UFBX_EXTRAPOLATION_CONSTANT) {
+		return key->value;
+	} else if (ext.mode == UFBX_EXTRAPOLATION_SLOPE) {
+		ufbx_tangent tangent = *(pre ? &key->right : &key->left);
+		return key->value + (ufbx_real)(tangent.dy * ((real_time - key->time) / tangent.dx));
+	} else if (ext.repeat_count == 0) {
+		return key->value;
+	}
+
+	// Perform all operations in KTime ticks to be frame perfect
+	double scale = (double)curve->element.scene->metadata.ktime_second;
+	double min_time = ufbx_rint(curve->min_time * scale);
+	double max_time = ufbx_rint(curve->max_time * scale);
+	double time = real_time * scale;
+
+	double delta = pre ? min_time - time : time - max_time;
+	double duration = max_time - min_time;
+
+	// Require at least one KTime unit
+	if (!(duration >= 1.0)) return key->value;
+
+	double rep = delta / duration;
+	double rep_n = ufbx_floor(rep);
+	double rep_d = delta - rep_n * duration;
+
+	if (ext.repeat_count > 0 && rep_n >= (double)ext.repeat_count) {
+		// Clamp to the repeat count to handle mirroring
+		rep_n = (double)(ext.repeat_count - 1);
+		rep_d = duration;
+	}
+
+	if (ext.mode == UFBX_EXTRAPOLATION_MIRROR) {
+		double rep_parity = rep_n*0.5 - ufbx_floor(rep_n*0.5);
+		if (rep_parity <= 0.25) {
+			rep_d = duration - rep_d;
+		}
+	}
+
+	if (pre) rep_d = duration - rep_d;
+	double new_time = (min_time + rep_d) / scale;
+
+	ufbx_real value = ufbx_evaluate_curve_flags(curve, new_time, key->value, flags | UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION);
+
+	if (ext.mode == UFBX_EXTRAPOLATION_REPEAT_RELATIVE) {
+		ufbx_real val_delta = curve->keyframes.data[curve->keyframes.count - 1].value - curve->keyframes.data[0].value;
+		if (pre) val_delta = -val_delta;
+		value += val_delta * (ufbx_real)(rep_n + 1.0);
+	}
+
+	return value;
+}
+
 #if UFBXI_FEATURE_SCENE_EVALUATION
 
 typedef struct {
@@ -25686,7 +25938,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context
 		ufbx_prop *props = ufbxi_push(&ec->result, ufbx_prop, num_animated);
 		ufbxi_check_err(&ec->error, props);
 
-		elem->props = ufbx_evaluate_props(&anim, elem, ec->time, props, num_animated);
+		elem->props = ufbx_evaluate_props_flags(&anim, elem, ec->time, props, num_animated, ec->opts.evaluate_flags);
 		elem->props.defaults = &ec->src_scene.elements.data[elem->element_id]->props;
 	}
 
@@ -26693,6 +26945,9 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context
 		}
 
 		flags |= UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER|UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE|UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES;
+		if (bc->opts.evaluate_flags & UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION) {
+			flags |= UFBX_TRANSFORM_FLAG_NO_EXTRAPOLATION;
+		}
 
 		double eval_time = ufbxi_bake_time_sample_time(bake_time);
 		ufbx_transform transform = ufbx_evaluate_transform_flags(bc->anim, node, eval_time, flags);
@@ -26813,7 +27068,7 @@ ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_prop(ufbxi_bake_contex
 	for (size_t i = 0; i < times.count; i++) {
 		ufbxi_bake_time bake_time = times.data[i];
 		double eval_time = ufbxi_bake_time_sample_time(bake_time);
-		ufbx_prop prop = ufbx_evaluate_prop_len(bc->anim, element, name.data, name.length, eval_time);
+		ufbx_prop prop = ufbx_evaluate_prop_len_flags(bc->anim, element, name.data, name.length, eval_time, bc->opts.evaluate_flags);
 		keys.data[i].time = bake_time.time;
 		keys.data[i].value = prop.value_vec3;
 		keys.data[i].flags = (ufbx_baked_key_flags)bake_time.flags;
@@ -30113,6 +30368,11 @@ ufbx_abi ufbxi_noinline ufbx_matrix ufbx_get_compatible_matrix_for_normals(const
 }
 
 ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time, ufbx_real default_value)
+{
+	return ufbx_evaluate_curve_flags(curve, time, default_value, 0);
+}
+
+ufbx_abi ufbx_real ufbx_evaluate_curve_flags(const ufbx_anim_curve *curve, double time, ufbx_real default_value, uint32_t flags)
 {
 	if (!curve) return default_value;
 	if (curve->keyframes.count <= 1) {
@@ -30123,6 +30383,12 @@ ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time
 		}
 	}
 
+	if ((flags & UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION) == 0) {
+		if (time < curve->min_time || time > curve->max_time) {
+			return ufbxi_extrapolate_curve(curve, time, flags);
+		}
+	}
+
 	size_t begin = 0;
 	size_t end = curve->keyframes.count;
 	const ufbx_keyframe *keys = curve->keyframes.data;
@@ -30191,17 +30457,27 @@ ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time
 }
 
 ufbx_abi ufbxi_noinline ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time)
+{
+	return ufbx_evaluate_anim_value_real_flags(anim_value, time, 0);
+}
+
+ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time)
+{
+	return ufbx_evaluate_anim_value_vec3_flags(anim_value, time, 0);
+}
+
+ufbx_abi ufbxi_noinline ufbx_real ufbx_evaluate_anim_value_real_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags)
 {
 	if (!anim_value) {
 		return 0.0f;
 	}
 
 	ufbx_real res = anim_value->default_value.x;
-	if (anim_value->curves[0]) res = ufbx_evaluate_curve(anim_value->curves[0], time, res);
+	if (anim_value->curves[0]) res = ufbx_evaluate_curve_flags(anim_value->curves[0], time, res, flags);
 	return res;
 }
 
-ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time)
+ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags)
 {
 	if (!anim_value) {
 		ufbx_vec3 zero = { 0.0f };
@@ -30209,13 +30485,18 @@ ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_
 	}
 
 	ufbx_vec3 res = anim_value->default_value;
-	if (anim_value->curves[0]) res.x = ufbx_evaluate_curve(anim_value->curves[0], time, res.x);
-	if (anim_value->curves[1]) res.y = ufbx_evaluate_curve(anim_value->curves[1], time, res.y);
-	if (anim_value->curves[2]) res.z = ufbx_evaluate_curve(anim_value->curves[2], time, res.z);
+	if (anim_value->curves[0]) res.x = ufbx_evaluate_curve_flags(anim_value->curves[0], time, res.x, flags);
+	if (anim_value->curves[1]) res.y = ufbx_evaluate_curve_flags(anim_value->curves[1], time, res.y, flags);
+	if (anim_value->curves[2]) res.z = ufbx_evaluate_curve_flags(anim_value->curves[2], time, res.z, flags);
 	return res;
 }
 
 ufbx_abi ufbxi_noinline ufbx_prop ufbx_evaluate_prop_len(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time)
+{
+	return ufbx_evaluate_prop_len_flags(anim, element, name, name_len, time, 0);
+}
+
+ufbx_abi ufbxi_noinline ufbx_prop ufbx_evaluate_prop_len_flags(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time, uint32_t flags)
 {
 	ufbx_prop result;
 
@@ -30242,15 +30523,20 @@ ufbx_abi ufbxi_noinline ufbx_prop ufbx_evaluate_prop_len(const ufbx_anim *anim,
 	if ((result.flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_CONNECTED)) == 0) return result;
 
 	if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) {
-		ufbxi_evaluate_connected_prop(&result, anim, element, prop->name.data, time);
+		ufbxi_evaluate_connected_prop(&result, anim, element, prop->name.data, time, flags);
 	}
 
-	ufbxi_evaluate_props(anim, element, time, &result, 1);
+	ufbxi_evaluate_props(anim, element, time, &result, 1, flags);
 
 	return result;
 }
 
 ufbx_abi ufbxi_noinline ufbx_props ufbx_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size)
+{
+	return ufbx_evaluate_props_flags(anim, element, time, buffer, buffer_size, 0);
+}
+
+ufbx_abi ufbxi_noinline ufbx_props ufbx_evaluate_props_flags(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size, uint32_t flags)
 {
 	ufbx_props ret = { NULL };
 	if (!element) return ret;
@@ -30267,11 +30553,11 @@ ufbx_abi ufbxi_noinline ufbx_props ufbx_evaluate_props(const ufbx_anim *anim, co
 		*dst = *prop;
 
 		if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) {
-			ufbxi_evaluate_connected_prop(dst, anim, element, prop->name.data, time);
+			ufbxi_evaluate_connected_prop(dst, anim, element, prop->name.data, time, flags);
 		}
 	}
 
-	ufbxi_evaluate_props(anim, element, time, buffer, num_anim);
+	ufbxi_evaluate_props(anim, element, time, buffer, num_anim, flags);
 
 	ret.props.data = buffer;
 	ret.props.count = ret.num_animated = num_anim;
@@ -30382,8 +30668,13 @@ ufbx_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform_flags(const ufbx_
 		}
 	}
 
+	uint32_t eval_flags = 0;
+	if (flags & UFBX_TRANSFORM_FLAG_NO_EXTRAPOLATION) {
+		eval_flags |= UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION;
+	}
+
 	ufbx_prop buf[ufbxi_arraycount(ufbxi_transform_props_all)]; // ufbxi_uninit
-	ufbx_props props = ufbxi_evaluate_selected_props(anim, &node->element, time, buf, prop_names, num_prop_names);
+	ufbx_props props = ufbxi_evaluate_selected_props(anim, &node->element, time, buf, prop_names, num_prop_names, eval_flags);
 	ufbx_rotation_order order = (ufbx_rotation_order)ufbxi_find_enum(&props, ufbxi_RotationOrder, UFBX_ROTATION_ORDER_XYZ, UFBX_ROTATION_ORDER_SPHERIC);
 
 	ufbx_transform transform; // ufbxi_uninit
@@ -30412,13 +30703,18 @@ ufbx_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform_flags(const ufbx_
 }
 
 ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time)
+{
+	return ufbx_evaluate_blend_weight_flags(anim, channel, time, 0);
+}
+
+ufbx_abi ufbx_real ufbx_evaluate_blend_weight_flags(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time, uint32_t flags)
 {
 	const char *prop_names[] = {
 		ufbxi_DeformPercent,
 	};
 
 	ufbx_prop buf[ufbxi_arraycount(prop_names)]; // ufbxi_uninit
-	ufbx_props props = ufbxi_evaluate_selected_props(anim, &channel->element, time, buf, prop_names, ufbxi_arraycount(prop_names));
+	ufbx_props props = ufbxi_evaluate_selected_props(anim, &channel->element, time, buf, prop_names, ufbxi_arraycount(prop_names), flags);
 	return ufbxi_find_real(&props, ufbxi_DeformPercent, channel->weight * (ufbx_real)100.0) * (ufbx_real)0.01;
 }
 
@@ -32332,6 +32628,7 @@ ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack(const ufbx_scene *scene, const ch
 ufbx_abi ufbx_material *ufbx_find_material(const ufbx_scene *scene, const char *name) { return ufbx_find_material_len(scene, name, strlen(name)); }
 ufbx_abi ufbx_anim_prop *ufbx_find_anim_prop(const ufbx_anim_layer *layer, const ufbx_element *element, const char *prop) { return ufbx_find_anim_prop_len(layer, element, prop, strlen(prop)); }
 ufbx_abi ufbx_prop ufbx_evaluate_prop(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time) { return ufbx_evaluate_prop_len(anim, element, name, strlen(name), time); }
+ufbx_abi ufbx_prop ufbx_evaluate_prop_flags(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags) { return ufbx_evaluate_prop_len_flags(anim, element, name, strlen(name), time, flags); }
 ufbx_abi ufbx_texture *ufbx_find_prop_texture(const ufbx_material *material, const char *name) { return ufbx_find_prop_texture_len(material, name, strlen(name)); }
 ufbx_abi ufbx_string ufbx_find_shader_prop(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_len(shader, name, strlen(name)); }
 ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_bindings_len(shader, name, strlen(name)); }

+ 75 - 3
thirdparty/ufbx/ufbx.h

@@ -267,7 +267,7 @@ struct ufbx_converter { };
 // `ufbx_source_version` contains the version of the corresponding source file.
 // HINT: The version can be compared numerically to the result of `ufbx_pack_version()`,
 // for example `#if UFBX_VERSION >= ufbx_pack_version(0, 12, 0)`.
-#define UFBX_HEADER_VERSION ufbx_pack_version(0, 17, 1)
+#define UFBX_HEADER_VERSION ufbx_pack_version(0, 18, 0)
 #define UFBX_VERSION UFBX_HEADER_VERSION
 
 // -- Basic types
@@ -2353,6 +2353,9 @@ typedef enum ufbx_shader_type UFBX_ENUM_REPR {
 	// 3ds glTF Material
 	// https://help.autodesk.com/view/3DSMAX/2023/ENU/?guid=GUID-7ABFB805-1D9F-417E-9C22-704BFDF160FA
 	UFBX_SHADER_GLTF_MATERIAL,
+	// 3ds OpenPBR Material
+	// https://help.autodesk.com/view/3DSMAX/2025/ENU/?guid=GUID-CD90329C-1E2B-4BBA-9285-3BB46253B9C2
+	UFBX_SHADER_OPENPBR_MATERIAL,
 	// Stingray ShaderFX shader graph.
 	// Contains a serialized `"ShaderGraph"` in `ufbx_props`.
 	UFBX_SHADER_SHADERFX_GRAPH,
@@ -2437,6 +2440,7 @@ typedef enum ufbx_material_pbr_map UFBX_ENUM_REPR {
 	UFBX_MATERIAL_PBR_COAT_NORMAL,
 	UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_COLOR,
 	UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_ROUGHNESS,
+	UFBX_MATERIAL_PBR_THIN_FILM_FACTOR,
 	UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS,
 	UFBX_MATERIAL_PBR_THIN_FILM_IOR,
 	UFBX_MATERIAL_PBR_EMISSION_FACTOR,
@@ -2561,6 +2565,7 @@ typedef struct ufbx_material_pbr_maps {
 			ufbx_material_map coat_normal;
 			ufbx_material_map coat_affect_base_color;
 			ufbx_material_map coat_affect_base_roughness;
+			ufbx_material_map thin_film_factor;
 			ufbx_material_map thin_film_thickness;
 			ufbx_material_map thin_film_ior;
 			ufbx_material_map emission_factor;
@@ -3141,6 +3146,26 @@ typedef enum ufbx_interpolation UFBX_ENUM_REPR {
 
 UFBX_ENUM_TYPE(ufbx_interpolation, UFBX_INTERPOLATION, UFBX_INTERPOLATION_CUBIC);
 
+typedef enum ufbx_extrapolation_mode UFBX_ENUM_REPR {
+	UFBX_EXTRAPOLATION_CONSTANT,        // < Use the value of the first/last keyframe
+	UFBX_EXTRAPOLATION_REPEAT,          // < Repeat the whole animation curve
+	UFBX_EXTRAPOLATION_MIRROR,          // < Repeat with mirroring
+	UFBX_EXTRAPOLATION_SLOPE,           // < Use the tangent of the last keyframe to linearly extrapolate
+	UFBX_EXTRAPOLATION_REPEAT_RELATIVE, // < Repeat the animation curve but connect the first and last keyframe values
+
+	UFBX_ENUM_FORCE_WIDTH(UFBX_EXTRAPOLATION)
+} ufbx_extrapolation_mode;
+
+UFBX_ENUM_TYPE(ufbx_extrapolation_mode, UFBX_EXTRAPOLATION_MODE, UFBX_EXTRAPOLATION_REPEAT_RELATIVE);
+
+typedef struct ufbx_extrapolation {
+	ufbx_extrapolation_mode mode;
+
+	// Count used for repeating modes.
+	// Negative values mean infinite repetition.
+	int32_t repeat_count;
+} ufbx_extrapolation;
+
 // Tangent vector at a keyframe, may be split into left/right
 typedef struct ufbx_tangent {
 	float dx; // < Derivative in the time axis
@@ -3177,10 +3202,21 @@ struct ufbx_anim_curve {
 		uint32_t typed_id;
 	}; };
 
+	// List of keyframes that define the curve.
 	ufbx_keyframe_list keyframes;
 
+	// Extrapolation before the curve.
+	ufbx_extrapolation pre_extrapolation;
+	// Extrapolation after the curve.
+	ufbx_extrapolation post_extrapolation;
+
+	// Value range for all the keyframes.
 	ufbx_real min_value;
 	ufbx_real max_value;
+
+	// Time range for all the keyframes.
+	double min_time;
+	double max_time;
 };
 
 // -- Collections
@@ -3501,6 +3537,10 @@ typedef enum ufbx_warning_type UFBX_ENUM_REPR {
 	// Missing polygon mapping type.
 	UFBX_WARNING_MISSING_POLYGON_MAPPING,
 
+	// Unsupported version, loaded but may be incorrect.
+	// If the loading fails `UFBX_ERROR_UNSUPPORTED_VERSION` is issued instead.
+	UFBX_WARNING_UNSUPPORTED_VERSION,
+
 	// Out-of-bounds index has been clamped to be in-bounds.
 	// HINT: You can use `ufbx_index_error_handling` to adjust behavior.
 	UFBX_WARNING_INDEX_CLAMPED,
@@ -4179,10 +4219,14 @@ typedef enum ufbx_error_type UFBX_ENUM_REPR {
 	// Duplicated override property in `ufbx_create_anim()`
 	UFBX_ERROR_DUPLICATE_OVERRIDE,
 
+	// Unsupported file format version.
+	// ufbx still tries to load files with unsupported versions, see `UFBX_WARNING_UNSUPPORTED_VERSION`.
+	UFBX_ERROR_UNSUPPORTED_VERSION,
+
 	UFBX_ENUM_FORCE_WIDTH(UFBX_ERROR_TYPE)
 } ufbx_error_type;
 
-UFBX_ENUM_TYPE(ufbx_error_type, UFBX_ERROR_TYPE, UFBX_ERROR_DUPLICATE_OVERRIDE);
+UFBX_ENUM_TYPE(ufbx_error_type, UFBX_ERROR_TYPE, UFBX_ERROR_UNSUPPORTED_VERSION);
 
 // Error description with detailed stack trace
 // HINT: You can use `ufbx_format_error()` for formatting the error
@@ -4598,6 +4642,15 @@ typedef struct ufbx_thread_opts {
 
 } ufbx_thread_opts;
 
+// Flags to control nanimation evaluation functions.
+typedef enum ufbx_evaluate_flags UFBX_FLAG_REPR {
+
+	// Do not extrapolate past the keyframes.
+	UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION = 0x1,
+
+	UFBX_FLAG_FORCE_WIDTH(ufbx_evaluate_flags)
+} ufbx_evaluate_flags;
+
 // -- Main API
 
 // Options for `ufbx_load_file/memory/stream/stdio()`
@@ -4784,7 +4837,7 @@ typedef struct ufbx_load_opts {
 	bool use_root_transform;
 	ufbx_transform root_transform;
 
-	// Animation keyframe clamp threhsold, only applies to specific interpolation modes.
+	// Animation keyframe clamp threshold, only applies to specific interpolation modes.
 	double key_clamp_threshold;
 
 	// Specify how to handle Unicode errors in strings.
@@ -4860,6 +4913,10 @@ typedef struct ufbx_evaluate_opts {
 	bool evaluate_skinning; // < Evaluate skinning (see ufbx_mesh.skinned_vertices)
 	bool evaluate_caches;   // < Evaluate vertex caches (see ufbx_mesh.skinned_vertices)
 
+	// Evaluation flags.
+	// See `ufbx_evaluate_flags` for information.
+	uint32_t evaluate_flags;
+
 	// WARNING: Potentially unsafe! Try to open external files such as geometry caches
 	bool load_external_files;
 
@@ -5001,6 +5058,10 @@ typedef struct ufbx_bake_opts {
 	// `time / (1.0 + step_custom_epsilon)` and `time * (1.0 + step_custom_epsilon)`.
 	double step_custom_epsilon;
 
+	// Flags passed to animation evaluation functions.
+	// See `ufbx_evaluate_flags`.
+	uint32_t evaluate_flags;
+
 	// Enable key reduction.
 	bool key_reduction_enabled;
 
@@ -5330,20 +5391,26 @@ ufbx_unsafe ufbx_abi bool ufbx_open_memory_ctx(ufbx_stream *stream, ufbx_open_fi
 // Evaluate a single animation `curve` at a `time`.
 // Returns `default_value` only if `curve == NULL` or it has no keyframes.
 ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time, ufbx_real default_value);
+ufbx_abi ufbx_real ufbx_evaluate_curve_flags(const ufbx_anim_curve *curve, double time, ufbx_real default_value, uint32_t flags);
 
 // Evaluate a value from bundled animation curves.
 ufbx_abi ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time);
 ufbx_abi ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time);
+ufbx_abi ufbx_real ufbx_evaluate_anim_value_real_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags);
+ufbx_abi ufbx_vec3 ufbx_evaluate_anim_value_vec3_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags);
 
 // Evaluate an animated property `name` from `element` at `time`.
 // NOTE: If the property is not found it will have the flag `UFBX_PROP_FLAG_NOT_FOUND`.
 ufbx_abi ufbx_prop ufbx_evaluate_prop_len(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time);
 ufbx_abi ufbx_prop ufbx_evaluate_prop(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time);
+ufbx_abi ufbx_prop ufbx_evaluate_prop_len_flags(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time, uint32_t flags);
+ufbx_abi ufbx_prop ufbx_evaluate_prop_flags(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags);
 
 // Evaluate all _animated_ properties of `element`.
 // HINT: This function returns an `ufbx_props` structure with the original properties as
 // `ufbx_props.defaults`. This lets you use `ufbx_find_prop/value()` for the results.
 ufbx_abi ufbx_props ufbx_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size);
+ufbx_abi ufbx_props ufbx_evaluate_props_flags(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size, uint32_t flags);
 
 // Flags to control `ufbx_evaluate_transform_flags()`.
 typedef enum ufbx_transform_flags UFBX_FLAG_REPR {
@@ -5366,6 +5433,10 @@ typedef enum ufbx_transform_flags UFBX_FLAG_REPR {
 	// If `UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES`: Evaluate `ufbx_transform.scale`.
 	UFBX_TRANSFORM_FLAG_INCLUDE_SCALE = 0x40,
 
+	// Do not extrapolate keyframes.
+	// See `UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION`.
+	UFBX_TRANSFORM_FLAG_NO_EXTRAPOLATION = 0x80,
+
 	UFBX_FLAG_FORCE_WIDTH(UFBX_TRANSFORM_FLAGS)
 } ufbx_transform_flags;
 
@@ -5378,6 +5449,7 @@ ufbx_abi ufbx_transform ufbx_evaluate_transform_flags(const ufbx_anim *anim, con
 // Evaluate the blend shape weight of a blend channel.
 // NOTE: Return value uses `1.0` for full weight, instead of `100.0` that the internal property `UFBX_Weight` uses.
 ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time);
+ufbx_abi ufbx_real ufbx_evaluate_blend_weight_flags(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time, uint32_t flags);
 
 // Evaluate the whole `scene` at a specific `time` in the animation `anim`.
 // The returned scene behaves as if it had been exported at a specific time