Blender V5.0
obj_export_mtl.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
8
9#include "BKE_image.hh"
10#include "BKE_node.hh"
12#include "BKE_node_runtime.hh"
13
14#include "BLI_math_vector.h"
15#include "BLI_path_utils.hh"
16#include "BLI_string.h"
17
18#include "DNA_material_types.h"
19#include "DNA_node_types.h"
20
21#include "obj_export_mtl.hh"
22
23#include "CLG_log.h"
24static CLG_LogRef LOG = {"io.obj"};
25
26namespace blender::io::obj {
27
29 "Base Color",
30 "Metallic",
31 "Specular IOR Level",
32 "Roughness", /* Map specular exponent to roughness. */
33 "Roughness",
34 "Sheen Weight",
35 "Metallic", /* Map reflection to metallic. */
36 "Emission Color",
37 "Alpha",
38 "Normal",
39};
41 "array size mismatch");
42
46static void copy_property_from_node(const eNodeSocketDatatype property_type,
47 const bNode *node,
48 const char *identifier,
49 MutableSpan<float> r_property)
50{
51 if (!node) {
52 return;
53 }
54 const bNodeSocket *socket = bke::node_find_socket(
55 *const_cast<bNode *>(node), SOCK_IN, identifier);
56 BLI_assert(socket && socket->type == property_type);
57 if (!socket) {
58 return;
59 }
60 switch (property_type) {
61 case SOCK_FLOAT: {
62 BLI_assert(r_property.size() == 1);
63 const bNodeSocketValueFloat *socket_def_value = static_cast<const bNodeSocketValueFloat *>(
64 socket->default_value);
65 r_property[0] = socket_def_value->value;
66 break;
67 }
68 case SOCK_RGBA: {
69 BLI_assert(r_property.size() == 3);
70 const bNodeSocketValueRGBA *socket_def_value = static_cast<const bNodeSocketValueRGBA *>(
71 socket->default_value);
72 copy_v3_v3(r_property.data(), socket_def_value->value);
73 break;
74 }
75 case SOCK_VECTOR: {
76 BLI_assert(r_property.size() == 3);
77 const bNodeSocketValueVector *socket_def_value = static_cast<const bNodeSocketValueVector *>(
78 socket->default_value);
79 copy_v3_v3(r_property.data(), socket_def_value->value);
80 break;
81 }
82 default: {
83 /* Other socket types are not handled here. */
84 BLI_assert(0);
85 break;
86 }
87 }
88}
89
93static void linked_sockets_to_dest_id(const bNode *dest_node,
94 const bNodeTree &node_tree,
95 const char *dest_socket_id,
96 Vector<const bNodeSocket *> &r_linked_sockets)
97{
98 r_linked_sockets.clear();
99 if (!dest_node) {
100 return;
101 }
102 Span<const bNode *> object_dest_nodes = node_tree.nodes_by_type(dest_node->idname);
103 Span<const bNodeSocket *> dest_inputs = object_dest_nodes.first()->input_sockets();
104 const bNodeSocket *dest_socket = nullptr;
105 for (const bNodeSocket *curr_socket : dest_inputs) {
106 if (STREQ(curr_socket->identifier, dest_socket_id)) {
107 dest_socket = curr_socket;
108 break;
109 }
110 }
111 if (dest_socket) {
112 Span<const bNodeSocket *> linked_sockets = dest_socket->directly_linked_sockets();
113 r_linked_sockets.resize(linked_sockets.size());
114 r_linked_sockets = linked_sockets;
115 }
116}
117
121static const bNode *get_node_of_type(Span<const bNodeSocket *> sockets_list, const int node_type)
122{
123 for (const bNodeSocket *socket : sockets_list) {
124 const bNode &parent_node = socket->owner_node();
125 if (parent_node.typeinfo->type_legacy == node_type) {
126 return &parent_node;
127 }
128 }
129 return nullptr;
130}
131
132/*
133 * From a texture image shader node, get the image's filepath.
134 * If packed image is found, only the file "name" is returned.
135 */
136static std::string get_image_filepath(const bNode *tex_node)
137{
138 if (!tex_node) {
139 return "";
140 }
141 Image *tex_image = reinterpret_cast<Image *>(tex_node->id);
142 if (!tex_image || !BKE_image_has_filepath(tex_image)) {
143 return "";
144 }
145
146 if (BKE_image_has_packedfile(tex_image)) {
147 /* Put image in the same directory as the `.MTL` file. */
148 const char *filename = BLI_path_basename(tex_image->filepath);
149 CLOG_INFO(&LOG,
150 "Packed image found:'%s'. Unpack and place the image in the same "
151 "directory as the .MTL file.",
152 filename);
153 return filename;
154 }
155
156 char filepath[FILE_MAX];
157 STRNCPY(filepath, tex_image->filepath);
158
159 if (tex_image->source == IMA_SRC_SEQUENCE) {
160 char head[FILE_MAX], tail[FILE_MAX];
161 ushort numlen;
162 int framenr = static_cast<NodeTexImage *>(tex_node->storage)->iuser.framenr;
163 BLI_path_sequence_decode(filepath, head, sizeof(head), tail, sizeof(tail), &numlen);
164 BLI_path_sequence_encode(filepath, sizeof(filepath), head, tail, numlen, framenr);
165 }
166
167 return filepath;
168}
169
175static const bNode *find_bsdf_node(const bNodeTree *nodetree)
176{
177 if (!nodetree) {
178 return nullptr;
179 }
180 for (const bNode *node : nodetree->nodes_by_type("ShaderNodeOutputMaterial")) {
181 const bNodeSocket &node_input_socket0 = node->input_socket(0);
182 for (const bNodeSocket *out_sock : node_input_socket0.directly_linked_sockets()) {
183 const bNode &in_node = out_sock->owner_node();
184 if (in_node.typeinfo->type_legacy == SH_NODE_BSDF_PRINCIPLED) {
185 return &in_node;
186 }
187 }
188 }
189 return nullptr;
190}
191
195static void store_bsdf_properties(const bNode *bsdf_node,
196 const Material *material,
197 MTLMaterial &r_mtl_mat)
198{
199 float roughness = material->roughness;
200 if (bsdf_node) {
201 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Roughness", {&roughness, 1});
202 }
203 /* Empirical approximation. Importer should use the inverse of this method. */
204 float spec_exponent = (1.0f - roughness);
205 spec_exponent *= spec_exponent * 1000.0f;
206
207 float specular = material->spec;
208 if (bsdf_node) {
209 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Specular IOR Level", {&specular, 1});
210 }
211
212 float metallic = material->metallic;
213 if (bsdf_node) {
214 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Metallic", {&metallic, 1});
215 }
216
217 float refraction_index = 1.0f;
218 if (bsdf_node) {
219 copy_property_from_node(SOCK_FLOAT, bsdf_node, "IOR", {&refraction_index, 1});
220 }
221
222 float alpha = material->a;
223 if (bsdf_node) {
224 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Alpha", {&alpha, 1});
225 }
226 const bool transparent = alpha != 1.0f;
227
228 float3 diffuse_col = {material->r, material->g, material->b};
229 if (bsdf_node) {
230 copy_property_from_node(SOCK_RGBA, bsdf_node, "Base Color", {diffuse_col, 3});
231 }
232
233 float3 emission_col{0.0f};
234 float emission_strength = 0.0f;
235 if (bsdf_node) {
236 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Emission Strength", {&emission_strength, 1});
237 copy_property_from_node(SOCK_RGBA, bsdf_node, "Emission Color", {emission_col, 3});
238 }
239 mul_v3_fl(emission_col, emission_strength);
240
241 float sheen = -1.0f;
242 float coat = -1.0f;
243 float coat_roughness = -1.0f;
244 float aniso = -1.0f;
245 float aniso_rot = -1.0f;
246 float transmission = -1.0f;
247 if (bsdf_node) {
248 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Sheen Weight", {&sheen, 1});
249 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Coat Weight", {&coat, 1});
250 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Coat Roughness", {&coat_roughness, 1});
251 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Anisotropic", {&aniso, 1});
252 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Anisotropic Rotation", {&aniso_rot, 1});
253 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Transmission Weight", {&transmission, 1});
254
255 /* Clearcoat used to include an implicit 0.25 factor, so stay compatible to old versions. */
256 coat *= 4.0f;
257 }
258
259 /* See https://wikipedia.org/wiki/Wavefront_.obj_file for all possible values of `illum`. */
260 /* Highlight on. */
261 int illum = 2;
262 if (specular == 0.0f) {
263 /* Color on and Ambient on. */
264 illum = 1;
265 }
266 else if (metallic > 0.0f) {
267 /* Metallic ~= Reflection. */
268 if (transparent) {
269 /* Transparency: Refraction on, Reflection: ~~Fresnel off and Ray trace~~ on. */
270 illum = 6;
271 }
272 else {
273 /* Reflection on and Ray trace on. */
274 illum = 3;
275 }
276 }
277 else if (transparent) {
278 /* Transparency: Glass on, Reflection: Ray trace off */
279 illum = 9;
280 }
281 r_mtl_mat.spec_exponent = spec_exponent;
282 if (metallic != 0.0f) {
283 r_mtl_mat.ambient_color = {metallic, metallic, metallic};
284 }
285 else {
286 r_mtl_mat.ambient_color = {1.0f, 1.0f, 1.0f};
287 }
288 r_mtl_mat.color = diffuse_col;
289 r_mtl_mat.spec_color = {specular, specular, specular};
290 r_mtl_mat.emission_color = emission_col;
291 r_mtl_mat.ior = refraction_index;
292 r_mtl_mat.alpha = alpha;
293 r_mtl_mat.illum_mode = illum;
294 r_mtl_mat.roughness = roughness;
295 r_mtl_mat.metallic = metallic;
296 r_mtl_mat.sheen = sheen;
297 r_mtl_mat.cc_thickness = coat;
298 r_mtl_mat.cc_roughness = coat_roughness;
299 r_mtl_mat.aniso = aniso;
300 r_mtl_mat.aniso_rot = aniso_rot;
301 r_mtl_mat.transmit_color = {transmission, transmission, transmission};
302}
303
307static void store_image_textures(const bNode *bsdf_node,
308 const bNodeTree *node_tree,
309 const Material *material,
310 MTLMaterial &r_mtl_mat)
311{
312 if (!material || !node_tree || !bsdf_node) {
313 /* No nodetree, no images, or no Principled BSDF node. */
314 return;
315 }
316
317 /* Normal Map Texture has two extra tasks of:
318 * - finding a Normal Map node before finding a texture node.
319 * - finding "Strength" property of the node for `-bm` option.
320 */
321
322 for (int key = 0; key < int(MTLTexMapType::Count); ++key) {
323 MTLTexMap &value = r_mtl_mat.texture_maps[key];
324 Vector<const bNodeSocket *> linked_sockets;
325 const bNode *normal_map_node{nullptr};
326
327 if (key == int(MTLTexMapType::Normal)) {
328 /* Find sockets linked to destination "Normal" socket in P-BSDF node. */
329 linked_sockets_to_dest_id(bsdf_node, *node_tree, "Normal", linked_sockets);
330 /* Among the linked sockets, find Normal Map shader node. */
331 normal_map_node = get_node_of_type(linked_sockets, SH_NODE_NORMAL_MAP);
332
333 /* Find sockets linked to "Color" socket in normal map node. */
334 linked_sockets_to_dest_id(normal_map_node, *node_tree, "Color", linked_sockets);
335 }
336 else {
337 /* Skip emission map if emission strength is zero. */
338 if (key == int(MTLTexMapType::Emission)) {
339 float emission_strength = 0.0f;
341 SOCK_FLOAT, bsdf_node, "Emission Strength", {&emission_strength, 1});
342 if (emission_strength == 0.0f) {
343 continue;
344 }
345 }
346 /* Find sockets linked to the destination socket of interest, in P-BSDF node. */
348 bsdf_node, *node_tree, tex_map_type_to_socket_id[key], linked_sockets);
349 }
350
351 /* Among the linked sockets, find Image Texture shader node. */
352 const bNode *tex_node{get_node_of_type(linked_sockets, SH_NODE_TEX_IMAGE)};
353 if (!tex_node) {
354 continue;
355 }
356 const std::string tex_image_filepath = get_image_filepath(tex_node);
357 if (tex_image_filepath.empty()) {
358 continue;
359 }
360
361 /* Find "Mapping" node if connected to texture node. */
362 linked_sockets_to_dest_id(tex_node, *node_tree, "Vector", linked_sockets);
363 const bNode *mapping = get_node_of_type(linked_sockets, SH_NODE_MAPPING);
364
365 if (normal_map_node) {
367 SOCK_FLOAT, normal_map_node, "Strength", {&r_mtl_mat.normal_strength, 1});
368 }
369 /* Texture transform options. Only translation (origin offset, "-o") and scale
370 * ("-o") are supported. */
371 copy_property_from_node(SOCK_VECTOR, mapping, "Location", {value.translation, 3});
372 copy_property_from_node(SOCK_VECTOR, mapping, "Scale", {value.scale, 3});
373
374 value.image_path = tex_image_filepath;
375 }
376}
377
379{
380 BLI_assert(material != nullptr);
381 MTLMaterial mtlmat;
382 mtlmat.name = std::string(material->id.name + 2);
383 std::replace(mtlmat.name.begin(), mtlmat.name.end(), ' ', '_');
384 const bNodeTree *nodetree = material->nodetree;
385 if (nodetree != nullptr) {
386 nodetree->ensure_topology_cache();
387 }
388
389 const bNode *bsdf_node = find_bsdf_node(nodetree);
390 store_bsdf_properties(bsdf_node, material, mtlmat);
391 store_image_textures(bsdf_node, nodetree, material, mtlmat);
392 return mtlmat;
393}
394
395} // namespace blender::io::obj
bool BKE_image_has_filepath(const Image *ima)
bool BKE_image_has_packedfile(const Image *image)
#define SH_NODE_TEX_IMAGE
#define SH_NODE_BSDF_PRINCIPLED
#define SH_NODE_NORMAL_MAP
#define SH_NODE_MAPPING
#define BLI_STATIC_ASSERT(a, msg)
Definition BLI_assert.h:83
#define BLI_assert(a)
Definition BLI_assert.h:46
MINLINE void mul_v3_fl(float r[3], float f)
MINLINE void copy_v3_v3(float r[3], const float a[3])
void void void const char * BLI_path_basename(const char *path) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT
#define FILE_MAX
int BLI_path_sequence_decode(const char *path, char *head, size_t head_maxncpy, char *tail, size_t tail_maxncpy, unsigned short *r_digits_len)
Definition path_utils.cc:58
void BLI_path_sequence_encode(char *path, size_t path_maxncpy, const char *head, const char *tail, unsigned short numlen, int pic)
char * STRNCPY(char(&dst)[N], const char *src)
Definition BLI_string.h:693
unsigned short ushort
#define ARRAY_SIZE(arr)
#define STREQ(a, b)
#define CLOG_INFO(clg_ref,...)
Definition CLG_log.h:190
@ IMA_SRC_SEQUENCE
@ SOCK_IN
eNodeSocketDatatype
@ SOCK_VECTOR
@ SOCK_FLOAT
@ SOCK_RGBA
constexpr int64_t size() const
Definition BLI_span.hh:493
constexpr T * data() const
Definition BLI_span.hh:539
constexpr const T & first() const
Definition BLI_span.hh:315
constexpr int64_t size() const
Definition BLI_span.hh:252
void resize(const int64_t new_size)
#define LOG(level)
Definition log.h:97
bNodeSocket * node_find_socket(bNode &node, eNodeSocketInOut in_out, StringRef identifier)
Definition node.cc:2532
const char * tex_map_type_to_socket_id[]
static const bNode * get_node_of_type(Span< const bNodeSocket * > sockets_list, const int node_type)
static std::string get_image_filepath(const bNode *tex_node)
static const bNode * find_bsdf_node(const bNodeTree *nodetree)
static void store_bsdf_properties(const bNode *bsdf_node, const Material *material, MTLMaterial &r_mtl_mat)
MTLMaterial mtlmaterial_for_material(const Material *material)
static void copy_property_from_node(const eNodeSocketDatatype property_type, const bNode *node, const char *identifier, MutableSpan< float > r_property)
static void store_image_textures(const bNode *bsdf_node, const bNodeTree *node_tree, const Material *material, MTLMaterial &r_mtl_mat)
static void linked_sockets_to_dest_id(const bNode *dest_node, const bNodeTree &node_tree, const char *dest_socket_id, Vector< const bNodeSocket * > &r_linked_sockets)
VecBase< float, 3 > float3
closure color sheen(normal N, float roughness) BUILTIN
char name[258]
Definition DNA_ID.h:432
char filepath[1024]
short source
struct bNodeTree * nodetree
void * default_value
bNodeTypeHandle * typeinfo
struct ID * id
void * storage
char idname[64]
MTLTexMap texture_maps[int(MTLTexMapType::Count)]